ReactJS Best Practices
When you start a new project in ReactJS from scratch, you must have searched various articles on the Internet about architecture and best practices. But most of them talk about the structure of the application and miss the important details.
In this article, we will look into some myths and guidance that will help you to create the best ReactJS project. All below pointers are based on my experience after working in multiple enterprise-level solutions. Let’s start.
When we have a big application in hand to be designed, it is crucial to divide the whole application into feature modules. It has the below benefits:
- Easy to maintain the code
- You can lazy load the whole module instead of separate pages
- Your modules work in isolation so if there is a bug in one module, it will not break the entire application
I like to prefer the following structure in my applications. Here, the core module holds common functionality, the home module is for layout/placeholder for other sub-modules, practice and learn are feature modules.
Services in ReactJS
If you come from the Angular world, then you must have got disappointed by knowing that there is no such thing called services in ReactJS. Services are meant to put common business logic in one place so that all components can take benefit from it. So, the good news is that you CAN create singleton services in ReactJS. The only difference is that you need to create the objects manually, but of course just once. There are two ways to create them.
Using Context API
1. Create Service
You cannot achieve good performance by just loading modules lazily. You need to take care of each and every small thing in your code seriously. This is only possible if you know how React works behind the scenes. Having a lot of code in the system doesn’t degrade the performance, but the code which takes a lot of time in computing and rendering, even though it is very small, is the real bottleneck.
Below is the code where <ChildComponent> renders whenever there is a state update in the input variable. There is no reason for the child component to rerender because its parent component is rendering on every input change.
To solve the above problem we can move the input state variable to its own component so only that particular component will rerender. In short, you are moving the state to its own component.
Like this, we should avoid unnecessary rendering of components wherever it is possible. Look into useMemo() and useCallback() hooks in detail and you will have a better idea about it.
Avoid Memory Leaks
There are many reasons for memory leaks in the application. One of them is to update the component even if it doesn’t exist in the DOM.
To avoid such scenarios, always clear your subscriptions. If there is no way you can unsubscribe them, then check if the component is still in DOM (or mounted) and then update the state. You can create your custom hook to check this.
And use it this way:
Encrypt Data in Local Storage
We often store JWT tokens and user details in local storage in plain format. It’s a bad idea. We should try our best to protect user data as much as possible. No one would use your application no matter how beautiful your pages are if you compromise the security aspect. In the below example you can see all information in the account object is encrypted.
When you are running your application in development mode and if there is any error, it breaks the whole application and shows the big red colour error in your browser for you to figure out what went wrong. The next step you do is to add ErrorBoundary (you can also use it in functional components) in your application to muffle the errors and let your application function (except the small part where the error came). But that doesn’t mean there is no error. What if this happens in the production environment? You cannot go and ask your users to send you the screenshots of consoles for stack trace.
So, what you need is to log in the first place. Whether it is dev, QA, UAT or Prod environment, you always need to find out the root cause of the error and that is only possible if you have a good logging solution implemented. Below are the recommend third-party solutions that you can use — don’t build your own solution.
There are many beautiful UI libraries available for you to pick up. I highly suggest you create wrapper components around the components offered by the UI libraries. For example, if your application has a lot of input controls, please create a wrapper around the input control of the library and use that instead. This will allow you to add more functionality on top of what you are already getting.
In the below example, I am using Ant Design’s Image component inside the wrapper called CBImage and I use this wrapper wherever I need the Image component. You can call CBImage as a widget. Just prefix any widget/wrapper you create with your product’s initial letters to avoid confusion with library components.
Write Transformation Layer
As a frontend developer, before starting your work on any story, you always discuss with the backend developer what the JSON structure of any object will be. Well, that is a good practice, but what if the backend team is lagging behind in terms of development? Will you sit idle for few days? Of course not. What you can do is create your own View Models and start designing the application with test (or you can call it stub) data. And when the time comes for integration with APIs, you just transform your model into the model that is expected by the backend.
The model which the frontend needs will be suffixed with VM and the model that the backend needs with be suffixed with DTO. In the below example, you can see that the backend response is converted to what the frontend needs. It also offers you greater flexibility out of the box. Let’s say if the backend developer decides to change any of the properties, you just need to update this code and nothing else. Your components would still work as if they don’t know any change happened.
Focus on Maintainability
The best feature of modern frameworks is the component which you can reuse at multiple places. But if that same component is given multiple responsibilities to handle, then you are misusing the component. A component should be designed with one feature in mind. Bypassing multiple props to the component and using if-else conditions in the component to render different layouts is a bad idea. Ideally, you are trying to save development hours in the beginning but as time passes that same component will start accommodating more and more features and very soon it will become a huge component.
Now let some months pass, and you come back to modify the component either due to a bug or to add a new feature. Even though the code was written by you, it will take some time to understand what you had written long back. Assuming your memory is good and you remember everything vividly, you start injecting additional code and test the functionality. But the time it took you to add new code would be longer this time and there are high chances that you might introduce new bugs. The reason? This same component is used at multiple places and you might have forgotten to verify all the scenarios.
For example, if you are supposed to create a responsive site, and you need your header to look different on different devices, then it’s better to create two separate components.
It is believed that if you don’t focus on maintainability from the beginning, your maintenance cost will be higher than the development.
Props Drilling is NOT bad
I have often seen people getting angry with what is called props drilling when they have a long hierarchy of components communication. And to prevent this they fall back on state management solutions like Redux or Akita. Well, such solutions are very good to handle communication where there is no parent-child relationship. But when you start using them in the parent-child hierarchy you are making your components less reusable. Based on my experience, it is always good to go with props drilling if you have a three-level hierarchy. If you have more, then opt for Context API.
If you have an enterprise application that has multiple portals then monorepo is for you. It offers you high reusability with better code management. Rather than creating multiple repositories, create small independent projects/modules into one big repository. Through this way, you can extract common components like Auth (login/logout/forgot password), UI library, etc into a shared module. All modules will have their own package.json file, so it will be very easy to manage dependencies. One such framework you can use is Nrwl.
You will never regret it if you follow the above practices. I will come up with more such practices in the coming days. Till then if you want me to go into detail in any of the aspects, please free to request in the comment section below.