Through my experience building and maintaining a sizable React/Next.js application, I have gained valuable insights applicable to projects. The most crucial aspect of any application is delivering a product that boasts exceptional user experience, speed, and seamless usability. It should be scalable and sustainable.
I make it a point to remember these factors in all my development work, and I hope they will be beneficial to you as well!
1. Plan the Folder Structure
Before starting any application, a plan for good folder structure is essential. Though it completely depends on the size of the team, application, and sometimes personal choices it is best to maintain a proper folder structure. For reference please visit this blog which explains 5 different application sizes and how to organise the files within folders for the same. For NextJS, file-based routing is followed so the folder structure will vary accordingly. Try to keep the same clean and reachable.
2. Maintain a Proper Import Order
There will definitely be a case while writing react code where the number of imports takes up a huge number of lines. It’s better to maintain the order using a linter or keeping the import sequence as BuiltIn -> ThirdParty -> Internal
3. Use Typescript
Next.js docs say:
“By default, Next.js will do type-checking as part of the build. We recommend using code editor type checking while development.”
While working with a large codebase Typescript can help developers to eliminate bugs even before the application is built. As Next.js checks for type definition during build time, the build will fail if any type mismatches are found in the code.
4. Use of Functional Components
To prevent ending up with code that is difficult to maintain and scale, it’s important to learn various component patterns. However, simply knowing the different patterns isn’t enough. Understanding when to use each pattern to solve specific problems is crucial. Each pattern serves a unique purpose. For instance,
Stateless components also called functional or presentational components always render the same thing or only what is passed to them via props. As a developer, we should always create stateless components.
Similarly, the compound component pattern can eliminate the need for passing props through multiple levels of components. If you see your code pass props through five component levels to reach the intended component, it may be time to reconsider how you’re organizing the components.
5. Use a Linter
In React/Next.js, ESLint has been configured already, but we can also set it up entirely on our own or extend the rules of a pre-configured ruleset.
6. Write Maintainable Tests
When the application grows, the complexity of the application also increases and in a complex application, refactoring becomes difficult as it is very hard to understand which files might break due to changes. It’s obvious that writing maintainable tests takes time, but I think that unit tests are crucial for large applications as it gives us the confidence to refactor files. Generally, I use Jest and React Testing Library for writing tests in my application.
7. Use Dependabot to Update Packages
In cases where multiple teams collaborate on the same application, it’s common for the packages used within the project to become outdated. This can occur because updating packages may involve addressing breaking changes, which could consume a significant amount of time and potentially delay the release of new features. Nevertheless, relying on outdated packages can cause issues in the long term, such as security vulnerabilities and performance problems.
Fortunately, tools like Dependabot can help by automating the update process. It can check for outdated packages and send updated pull requests whenever needed.
8. Use of Dynamic Imports/Code Splitting/ Lazy Loading
React doc says :
“Most React apps will have their files “bundled” using tools like Webpack, Rollup or Browserify. Bundling is the process of following imported files and merging them into a single file: a “bundle”. This bundle can then be included on a webpage to load an entire app at once.”
This is a great technique, but with the growth of our application, the bundle starts growing too. This bundle always loads completely, even when the user needs only a fragment of the code. This leads to performance issues as it can make our application take a very long time to load up.
To avoid this, there’s a technique called code splitting where we split up our bundle into the pieces of the code our user needs. This is supported by bundlers like Webpack and TurboPack. Splitting up our bundle helps to lazy load only the things that the user needs. React/Next.js supports lazy loading external libraries with next/dynamic, lazy and suspense. Check this for reference.
9. Reusable Logic Extraction into Custom Hooks
Whenever logic is being used in more than one component, it’s recommended to extract the logic into a custom hook. This custom hook encapsulates the logic and returns the desired value on passing different inputs. This makes a developer follow DRY(don’t repeat yourself) and removes the code redundancy in the application.
10. Effective Error Handling
When it comes to error handling there are three main steps to follow:
- Catch the errors
- Handle UI as per errors
- Log the errors
To catch the errors it’s better to wrap our error-prone components with Error Boundary. It will handle the UI in its render part as per our errors. The drawback with error boundaries is that it does not handle errors in asynchronous calls, event handlers and server-side rendering. For these exceptions, wrap them around try-catch blocks. Lastly, logging the errors properly is important and this should be done in one dedicated place. So it’s recommended to use a known third-party library named ‘Sentry’.Other monitoring services used can be Bugsnag or Datadog.
11. Use Tools like Storybook to Visualize UI Components
One way to manage UI components in a complex code base is by using Storybook, which provides a platform to visualize and organize components. With a large number of components in an application, it can be challenging to identify outdated or redundant components. Developers may create new components without realizing that similar functionality already exists. Storybook helps to keep track of all the components and their interactions with each other, enabling better management of the UI components in an application. Configuring and integrating the storybook with the existing next code is also easy.
12. Fetch Async Data using React Query/SWR.
Front-end applications typically fetch data from a back-end server to display it on the page. To do this in React/Next, developers can use the Fetch API, Axios, or similar libraries. However, as the application scales, it can become challenging to manage the asynchronous state of the data. Creating utility functions or wrappers around Fetch or Axios to manage these issues can lead to problems in caching and performance, especially when multiple developers work on the same application.
To avoid these issues, developers can use packages such as React Query or SWR. These packages offer default configurations out of the box, making it easier to handle tasks such as caching and performance. These packages provide some default configurations and options, allowing developers to customise their behaviours based on the application requirements. By fetching and caching async data from back-end API endpoints, these packages make the application state more maintainable.
Before deploying a web application to production, it is essential to follow various routing practices and clean code principles too. The official documentation provides several guidelines for these practices, including using consistent naming conventions, utilizing shorthand notation for boolean props, keeping the “key” prop unique, using fragments, and implementing other security measures. Adhering to these practices can significantly improve the overall quality of a developer’s code and ensure that their application is secure and performant. While the mentioned practices are fundamental, it is important to continually explore and implement additional best practices as they emerge to maintain the highest standards of code quality and application security.