Best Practices
In the world of software development, adhering to best practices is crucial for creating high-quality, maintainable, and efficient code. Best practices are a set of guidelines, principles, and techniques that have been proven effective over time and are widely accepted by the software development community. By following best practices, developers can write code that is easier to understand, test, debug, and maintain, ultimately leading to better software products. In this introduction, we will explore some of the key best practices for coding:
Readability and Maintainability
Writing code that is easy to read and understand is fundamental. This includes using meaningful variable and function/Components names, organizing code into logical blocks, adding comments where necessary, and following a consistent coding style. Code should be self-explanatory, allowing other developers (including your future self) to comprehend it quickly and make modifications without introducing errors.
❌ Non-Readable and Non-Maintainable Code:
// Poor variable naming and lack of explanation.let a = 5let b = 10
// Undescriptive function name and minimal context.function m(x, y) { return x + y}
// The purpose of these operations is unclear.let result = m(a, b)console.log(result)
✅ Readable and Maintainable Code:
// Declare and initialize two variables with descriptive namesconst firstNumber = 5const secondNumber = 10
// Define a function with a meaningful name that adds two numbersfunction addNumbers(x, y) { return x + y}
// Call the addNumbers function with the provided numbers and store the resultconst sum = addNumbers(firstNumber, secondNumber)
// Output the result with a clear and informative messageconsole.log(`The sum of ${firstNumber} and ${secondNumber} is ${sum}.`)
Modularity and Reusability
Breaking down code into smaller, modular components promotes code reusability and makes it easier to maintain. By designing code with a modular structure, individual functions can be reused across different parts of the application, reducing redundancy and improving efficiency. Well-designed modules also facilitate testing and debugging, as issues can be isolated and addressed more effectively.
Here's a very simple example comparing "bad" and "good" code examples in JavaScript to demonstrate the importance of code structure:
❌ Bad Code Example (Not Modular):
// Bad code: All logic is in a single blocklet radius = 5let area = Math.PI * radius * radiusconsole.log(`The area of the circle with a radius of ${radius} is ${area}.`)
let side = 4let squareArea = side * sideconsole.log(`The area of the square with a side length of ${side} is ${squareArea}.`)
In this "bad" example, all logic is combined into a single block. It calculates the area of a circle and the area of a square in the same code block, making it harder to maintain and reuse.
✅ Good Code Example (Modular):
// Good code: Modular structure with separate functionsfunction calculateCircleArea(radius) { return Math.PI * radius * radius}
function calculateSquareArea(side) { return side * side}
let circleRadius = 5let squareSide = 4
let circleArea = calculateCircleArea(circleRadius)let squareArea = calculateSquareArea(squareSide)
console.log(`The area of the circle with a radius of ${circleRadius} is ${circleArea}.`)console.log( `The area of the square with a side length of ${squareSide} is ${squareArea}.`)
In this "good" example:
The code is structured into smaller, modular functions
calculateCircleArea
andcalculateSquareArea
. Each function has a clear purpose, making the code more readable and maintainable.The logic for calculating the area of a circle and square is separated into distinct functions, promoting code reusability. If you need to calculate the area of another circle or square, you can easily reuse these functions.
Variable names are meaningful and descriptive, enhancing code readability.
The output is generated using the calculated values, making it clear and informative.
The "good" code example demonstrates the benefits of a modular code structure, making the code more maintainable and improving reusability.
Performance Optimization
Writing efficient code is crucial for optimizing resource utilization and enhancing the user experience. When incorporating a third-party library into your project, it is advisable to conduct a thorough analysis and comparison of available options. Prioritize selecting a library with a small file size and a high rating, ensuring it aligns with your project's requirements and objectives.
Why do you need to be careful about third-party scripts?
- They can be a performance concern
- They can be a privacy concern
- They might be a security concern
- They can be unpredictable and change without you knowing
- They can have unintended consequences
for example:
If you want a package that checks Password strength, You should use the check-password-strength@2.0.7
library instead of zxcvbn@4.4.2
for the following reasons:
File Size: zxcvbn
has a significantly larger file size of 799.5kb compared to check-password-strength
which is only 1kb. Choosing the smaller library aligns with the principle of optimizing resource utilization and reducing unnecessary overhead. This leads to faster load times and better user experiences, especially on slower network connections.
Active Development: zxcvbn
is an older library with its latest improvement dating back to 2017, while check-password-strength
received an improvement in 2022. Using a library that is actively maintained and updated is crucial for addressing security concerns, bug fixes, and compatibility with modern development practices. It demonstrates a commitment to keeping the library up to date and aligns with best practices for maintaining your codebase.
Before committing your code
It is essential to build and lint your project. Building the project ensures that all the code compiles correctly and any dependencies are resolved, while also catching any compilation errors or missing dependencies early on. This practice prevents issues from being introduced into the codebase.
Additionally, running a linter on your code helps enforce coding standards, such as formatting, naming conventions, and best practices, thus ensuring that the code is consistent, readable, and adheres to the guidelines set by the development team.
Furthermore, it's crucial to check for and fix any errors in the browser's devtools console before committing changes. By incorporating these practices into your workflow, you can maintain a clean and error-free codebase, making it easier for collaborators to understand, review, and work with your code.
Before committing changes, follow these steps:
- Run
yarn workspace WORKSPACE_NAME lint
to execute the linter on your code and ensure that it maintains consistency and adheres to the guidelines established by the development team. - Run
yarn workspace WORKSPACE_NAME build
to initiate the build process and verify its smooth operation. also don't forget to check the bundles size. - Run
yarn workspace WORKSPACE_NAME start
to launch the application in production mode. Please note that this requires a previous execution of yarn workspace WORKSPACE_NAME build to generate an optimized production build. - Thoroughly inspect the console for any errors in both development and production environments.
- In the production environment, remember to eliminate any console.log statements to prevent the inadvertent exposure of sensitive data and information to clients.
Using Prettier in Visual Studio Code
is highly beneficial and practically necessary for maintaining code consistency and formatting. Prettier is a popular code formatting tool that can automatically format your code according to a predefined set of rules. It ensures that the codebase adheres to a consistent style, including indentation, line breaks, and spacing, regardless of individual developer preferences. This consistency improves code readability, facilitates collaboration, and reduces the time spent on manual formatting. Prettier's integration with VSCode makes it seamless to format code with just a few clicks or even automatically upon saving a file. By utilizing Prettier in VSCode, developers can focus on writing code logic while leaving the formatting concerns to the tool, resulting in cleaner and more professional-looking code. You can add Prettier to your VSCode using the following Link.
Paying attention to HTML structure
While coding is crucial to prevent Next.js hydration errors. Hydration refers to the process of attaching event handlers and interactivity to a web page after it has been initially rendered on the server. When using Next.js, it is important to ensure that the HTML structure of the page is consistent between server-rendered and client-rendered content. Inconsistencies can lead to hydration errors, where the client-side JavaScript code expects a specific HTML structure that is not present. These errors can cause functionality issues or even break the application. By carefully structuring the HTML code, ensuring that server-rendered and client-rendered content match, developers can avoid hydration errors and ensure a smooth transition between server-side and client-side rendering in Next.js applications.
❌ Bad Code Example
// ❌ BAD: a div inside a paragraph<p> <div>Hello there Voider</div> <div>You are doing well</div></p>
// ❌ VERY BAD: a paragraph inside another paragraph<p> Hello there Voider <p>You are doing well</p></p>
// ❌ TERRIBLE: A link inside another link !!<Link href="#" variant="permalink"> Hello from Next.js Link Component <Link href="#" variant="primary"> Click here </Link></Link>
✅ Good Code Example
// ✅ GOOD: a paragraph inside a div<div> <p>Hello there Voider</p> <p>You are doing well</p></div>
// ✅ GREAT<p>Hello there Voider</p><p>You are doing well</p>
// ✅ EXCELLENT: Each link is independent from the other<Link href="#" variant="permalink"> Hello from Next.js Link Component</Link><Link href="#" variant="primary"> Click here</Link>
Using less CSS classes
In certain cases, it is beneficial to use fewer Tailwind CSS classes to enhance performance and readability. While these classes offer flexibility and convenience, using excessive or unnecessary classes can lead to larger file sizes and increased rendering times. By carefully considering the required styles and utilizing only the necessary Tailwind classes, developers can optimize performance by reducing the amount of CSS code that needs to be processed and loaded by the browser. Moreover, using fewer classes can improve code readability and maintainability, as it makes the purpose and intention of each class more evident. By striking a balance between using the power of Tailwind CSS and maintaining a lean codebase, developers can achieve improved performance, faster load times, and more readable CSS code.
Simply create your custom classes which depends on TailwindCSS, Please check Button
in apps/starter/components/elements/button/Button.jsx
for more details.
for example:
// ❌ Bad, Unreadable and Inefficient<Button className="inline-flex items-center focus:outline-none transition ease-in-out duration-300 border border-solid text-sm leading-[21px] font-bold text-center gap-2 px-[20px] py-[12px] rounded-[4px] transition transition-all ease-in-out duration-300 inline-flex items-center justify-center flex-row gap-[8px] text-white bg-success-500 border border-solid border-success-500 shadow-[0px_1px_2px_rgba(16,24,40,0.05)] hover:bg-success-600 hover:border-success-600 active:bg-success-700 active:border-success-700 whitespace-nowrap flex items-center gap-2 icon-trending-up-solid appearance-none text-sm leading-[21px]" href="/en/some_path"> Click me</Button>
// ✅ Good, Readable and Performant<Button className="btn btn-base btn_primary" href="/en/some_path"> Click me</Button>
Use Ternary Operator instead of Short Circuit in JSX
What's wrong with this code?
function ContactList({ contacts }) { return <>{contacts.length && <p>Some text</p>}</>}
Not sure? Let me ask you another question. What would happen with the above code if contacts was []
? That's right! You'd render 0
!
Why is that? Because when JavaScript evaluates 0 && something
, the result will always be 0
because 0
is falsy, so it doesn't evaluate the right side of the &&
.
The solution? Use a Ternary Operator to be explicit about what you want rendered in the falsy case. In our case, it was nothing (so using null is perfect):
function ContactList({ contacts }) { return <>{contacts.length ? <p>Some text</p> : null}</>}
What about this code? What's wrong here?
function Error({ error }) { return error && <div>{error.message}</div>}
Not sure? What if error is undefined
? If that's the case, you'll get the following error:
Or, in production, you might get this:
The problem here is that undefined && something
will always evaluate to undefined
.
the solution:
function Error({ error }) { return error ? <div>{error.message}</div> : null}
Better comments
We recommend using the Better Comments extension, It will help you create more human-friendly comments in your code. With this extension, you will be able to categorise your annotations into:
- Alerts
- Queries
- TODOs
- Highlights
- Commented out code can also be styled to make it clear the code shouldn't be there
- Any other comment styles you'd like can be specified in the settings
Example:
Analyzing your project
and closely monitoring the bundle size becomes essential whenever you introduce new libraries or large-sized components. The bundle size refers to the total size of the JavaScript, CSS, and other assets that need to be downloaded by the user's browser. As your project grows and evolves, it's crucial to assess the impact of each addition on the bundle size to ensure optimal performance and loading times. Adding large libraries or components can significantly increase the bundle size, which may result in slower page load times and a negative user experience. By regularly monitoring the bundle size and considering alternatives or optimizations, such as tree shaking, code splitting, or lazy loading, developers can mitigate the negative impact on performance and strive for a leaner and more efficient project. Making a conscious effort to analyze and manage the bundle size helps maintain a high-performing web application that delivers a smooth user experience. For more information check this section: Analyzing
Optimizing images in Next.js
is crucial for improving website performance and user experience. Images often contribute to the largest portion of a webpage's file size, impacting load times and overall performance. Next.js provides several techniques to optimize images efficiently. Firstly, Next.js automatically generates multiple image sizes for each uploaded image and serves the most appropriate version based on the device's screen size. This responsive image technique helps minimize unnecessary data transfer. Additionally, Next.js supports on-demand image resizing, allowing images to be dynamically resized and served in the optimal dimensions for the requesting device. By specifying width and height attributes for images, Next.js can generate and serve the exact size required, further reducing file size. Developers can also leverage third-party image optimization libraries or services, such as Next-optimized-images or Cloudinary, to further compress and optimize images for the web. By implementing these strategies, developers can significantly improve page load times, reduce bandwidth consumption, and enhance the overall performance of their Next.js applications. Read mroe about Next/Image.