When starting a new React project, it’s always a good idea to put together some guidelines that you and your team will follow to make the code scalable.
In this post, I will share with you a handful of insights from my years of using React that will help you to determine your own project guideline.
React is a library that manages the UI based on its current state. As a developer, it’s your job to organize where to keep the state that makes up your application. Some developers prefer to keep every single piece of data inside the redux store to keep track of all available states.
But do you really need to dispatch an action to your state manager just to open or close a simple dropdown menu? And do other parts of your application need to know about the value of that contact form? Form values tend to be short-lived and only used by the component that renders the form.
Rather than using Redux to keep track of every single state inside your application, it’s better to keep some state local to avoid over-engineering your application.
As a rule of thumb, you can ask these questions:
- Do other parts of the application care about this data?
- Do you need to be able to create further derived data based on this original data?
- Is the same data being used to drive multiple components?
- Is there value to you in being able to restore this state to a given point in time (ie, time travel debugging)?
- Do you want to cache the data (ie, use what’s in state if it’s already there instead of re-requesting it)?
- Do you want to keep this data consistent while hot-reloading UI components (which may lose their internal state when swapped)?
Components using local states are more independent and predictable.
The thing about writing automation tests is that at a certain point, it’s impossible to test your React project manually without spending a major amount of time and resources.
When starting out a project, it’s very easy to justify skipping writing test code because your codebase is relatively small. If you only have five to ten components in your React application, writing automation does feel like a chore with no clear benefit. But when you have more than fifty components and you have multiple higher-order components, testing your project manually might take a whole day, and even then there could be bugs creeping in without anyone noticing.
Yes, writing test code will help you make your code more modular. Yes, it will help you find errors faster and safeguard against crashing in production. But automation testing is ultimately about helping you grow your project when manual testing can no longer verify the code is working as expected.
But you can’t suddenly write test code when you’re not used to it. That’s why you have to start at the beginning. If you don’t know where to start, then start from integration test because the most important part about testing is that you verify that your components work together properly.
Normally, you don’t need to add many tools to your React project at the beginning of your application. But since we’re talking about scaling React application to a large codebase, I’d say you need to adopt all the good tools to help you out.
- Prettier and ESLint will be needed to give a consistent code pattern between team members and reduce syntax errors. Powerful utility libraries like React Router, date-fns, and react-hook-form are always good to include.
- Adding TypeScript and Redux may be delayed until your app is prone to typing errors and parts of your application require the same state over and over again that you need to make it globally available.
- Implementing state management from the start is not needed because React itself already thinks of the best way to manage state without making you crazy.
- Bit (Github) to manage and share your components as independent building blocks. That means you test and render each component in isolation. That will guarantee an easier time maintaining and reusing it, later on.
Moreover, when using Bit in your project, Bit source-controls each component independently (alongside your project’s SCM), which means a component (shared to Bit.dev) can be maintained completely independent of its project (i.e by “importing” a component from Bit.dev, changing it and “pushing” it back to its shared collection)
- Oh, and you can also use Next.js instead of Create React App to start your project.
These tools will help you maintain a large React codebase, but be aware that each tool you add will increase the complexity level of your project. Please do your research before deciding to adopt the tool into your stack.
One of the best tips that I learned on scaling React application is that organizing your project files and naming them well can speed up your progress. Some developers tend to write
index.js as the main file in a component directory, like this:
That seems reasonable because when you import the components into other files, the statement becomes simply this:
import Button from '../components/Button';
But consider when you open them side by side on the code editor:
Honestly, all those
index.js will make anyone confused. But if you rename those index.js files into the component name, your import statement will look a bit ugly:
import Button from '../components/Button/Button';
My team finally settled on having both the file named after the component and an
index.js file that exports the component:
We also put the CSS and unit test file inside the component directory. This way, each component directory can be a self-contained component.
You should not wait to build a component library only when your project has reached its large proportions. You can continuously share components as-you-go. Whenever a new component is built, use Bit to track it and share it to your team’s component collection on Bit.dev, or on your own server.
As mentioned earlier, (truly) independent components are much easier to maintain and when shared and documented, are much easier to reuse.
A component library is not only for UI components. Logic should also be included — in the case of React, as custom hooks (by and large).
Read more about it here:
As your project grows, you might notice that some of your component’s logic seems to get used over and over again. To share your component’s logic, you need to write a custom hook.
For example, let’s say you have an application that counts the score of a Basketball match:
As you can see in the example,
AwayTeam.js used the same logic for incrementing the counter. When you have the same logic responsible for driving UI changes, you can decouple the logic from your component and put it in a separate file.
In the example below, I removed the counter state and increment logic into a separate
util.js file and import it into the components:
Hooks are simply functions that return certain values back into its caller, that’s why you can implement the same pattern to reuse logic between your components.
Always remember that building React application at scale is a complicated task that requires you to consider the best decision for both the consumers and developers. In the end, the best practice is the one that works for your users and your team.
Although you will always have to experiment with tools and methods for scaling your React project, I hope the tips I’ve shared above will be useful for you.