This article outlines a set of best practices that will help with the development of large react applications.
For the past 3.5 years I have been working on a React project, which has now grown into a very large project with thousands of components and a lot more lines of codes. Over the years it went through several rounds of refactoring with lots of new architectural decisions and updated to the latest trends (e.g. react hooks).
This article uses the knowledge of the past years to outline some of the best practices in React.
We establish best practices to improve code quality but more importantly to minimize the decisions we have to take.
If we have to make new decisions all the time (e.g. naming conventions, file architecture e.t.c) we are increasing the time we spent thinking and if we are not careful the complexity of the project. As developers we have a limited bandwidth and we want to use as much of it to write meaningful code that will have a positive impact for the product and our customers. And to achieve this we need best practices.
1. Think in components
One thing that hasn’t change in the React ecosystem and will probably not change any time soon is the concept of components. Components have been a great way to encapsulate logic and that’s why we should strive for small, focused components which are easy to reason about, test and reuse.
For best results we should delegate any business logic into a nested component if possible. The deeper in the components tree the logic lives the better.
Take for example the following component, which uses a translation hook to translate a piece of text inside.
We could delegate the translation business logic into another component like so:
Then if we want to translate a piece of text we can use the
Here are the benefits we get from this refactoring:
- We managed to abstract away the logic for translation into the
Translationcomponent. Now there is a centralized translation component where we can change any UI or business logic if needed. If for example the
Translationcomponent was a legacy class component we could have updated it to use hooks (like above) and we would only have to do it in a single place before propagating to the entire project. Similarly if we want to add any UI logic to the translation (e.g. allow user to hover over the text to see the text in other languages) we only need to change a single file.
- It’s now a lot easier to test files that use translation. Before refactoring we would have to mock the
useTranslationsomehow because we don’t want to test its implementation. With the refactored code we only need to check that we render the Translation component and that we pass the correct props.
Tip: We should follow the same tactic when using third party libraries. We should wrap the library into a (nested) component. This way we have a central point to change any logic or just replace the third party library all-together.
2. Components should have flexible render methods
Components should allow flexibility to render in different shapes. This increases re-usability of components.
For example in every application there are components which perform an action when clicked. A button that creates a new task, a dropdown item that deletes a comment or a pencil icon that makes a piece of text editable. These components essentially call a method on click but we could potentially render them in different parts of our app with different shapes (Button in one place and dropdown item in another).
Therefore every app should have an action component that could look like this:
This component essentially gets an
onClick prop that will be called when any of the rendered sub-component is clicked. The component will take only one (can be achieved in typescript using
RequireExactlyOne from type-fest) of the remaining props. If it receives the
iconProps it will render an icon, if it receives
buttonProps it will render a button and so on. It also allows a
renderProp option where the parent component can take control and render whatever is needed.
Again, because this component is going to be a deeply nested component we can easily add more functionality to it, which will become available to our entire app. We could for example add functionality to open a modal and ask the user to confirm the action. We would just have to trigger the modal to open when the
Dropdown.Item is clicked and then call the
onClick prop when the modal confirmation button is clicked.
Note: The power of this component (especially the
renderProp will be better understood in the following section)
3. Generalize your components for all the entities in the system
In most apps we perform similar actions for different entities in the system. For example we can delete a
group e.t.c. Or we can set as favourite a
post or a
user. We are essentially performing the same action, the only thing that changes is the underlying entity. Therefore, it makes sense to create one reusable component to tackle this.
Generic Action component
A component to delete an entity could take the following form:
The two essential props that this component will take are the
entityType and the
entityType could be a
comment , a
post and so on depending on what entity we want to act on and the
entityId will be the id of the entity. With one component we can now delete any entity in our app.
Furthermore because we used the
Action component from the previous section we can render this action as an
So if we wanted to render a red, big button to delete a comment we would write something like this:
Or we could use the
renderProp to fully take control of what will be rendered by this component like this:
The same pattern could be extended for the other REST actions (Read, Create, Update or for GraphQL queries / mutations).
For people using
Redux combined with
connect function we can write a higher order component (HOC) to generalize how we connect to redux for actions to read any entity in the system.
The HOC above takes three props:
entityId are used in the
mapDispatchToProps to return the entity from the state and to return a function that dispatches an action to read the entity.
children is expected to be a function that will receive the
entity and the
And here’s how we would use the above to read a user:
If you want to find out how to structure Redux following the pattern above you can find a more detailed guide here: https://medium.com/hackernoon/state-management-with-redux-50f3ec10c10a
Other use cases
Another area where we could use this pattern is for forms. Forms are very similar across an app. The things that usually change are the input types (e.g. textarea, input, select) and the field names (depending on the entity). So it would make sense to have a single form component which takes a
fields prop and an
entityType prop in a similar fashion to the above example.
4. Keep state business logic separate from UI in a component
You should always strive to separate the state management logic from the ui logic. Having components that do both makes them less reusable, more difficult to test and more difficult to refactor when you want to change the way you do state management.
As we saw with the previous example, the
useDeleteEntity hook holds the logic to delete the entity, the
Action component holds the ui logic and then
EntityDelete brings the two together by implementing the delete logic and rendering the ui.
Here’s another example to emphasize this point:
Rather than writing the logic for setting the state inside a component we could extract it into a hook of its own. In the following example the toggling functionality is managed in the
onClick function of the button.
We could refactor the above to extract the toggling functionality into a
5. Embrace wishful thinking
A very helpful technique is to program by wishful thinking. What that means is that we can imagine that we have any component we wish in our app (even though it doesn’t exist yet) and start writing code as if they existed.
Say we want to load a list with the users of a group. What we want to do is to:
a) send an api request to fetch the users when the component mounts
b) show a placeholder until we get a response back from the server (to indicate to the user that something is being loaded)
c) once we get the response back show the list of the users.
Given these requirements we could write something like this:
What we are “wishing” here is to have a component called
Fetch , which will be responsible to do an api call and get users with some query parameters. While the api call is pending we want to show a placeholder and for this we are passing a component called
Placeholder , which can specify how it appears (in this case as a list). Once the api call is finished a component will be rendered specified by the children (as a function) passed. To do that the
Fetch component will call the children function with the
users fetched and as a result the
UserList component will be rendered.
In a few lines of code we loaded and displayed a list of users. And even though some of these components might not exist in the app, it’s now very easy to understand what needs to be developed. Even more, once the app has matured most of these components we “wished” were there, will actually be there, increasing the speed of development massively.
We could keep building on top of the above logic, adding even more props to the
Fetch component in order to specify pagination, sorting, filters e.t.c
6. Object oriented React
React has been all about functional programming. We can see with the introduction of hooks there is a strong desire to shift away from components written as classes and move entirely into a functional world. This is great for a few reasons but mainly because functional programming is very easy to reason about and testing becomes easy as well.
We could always though try different techniques and see if it makes sense in our apps. One such technique we found that works well in our app is to create classes for the data . We cast the json data we get from the api for each entity into its own class.
What we end up with is something like this:
Instead of using the json data we get from the api directly we could now create a class like so:
new Group(groupJsonDataFromApi) . Because this is now a class it unlocks some extra functionality.
In the file above we have one class for our
Post entity and one class for our
Group entity. They both share some fields like id and permissions (which we can extract into an
Entity class). We can add getters for each class like the
canDelete getter. This can compute if the authenticated user can delete a certain entity. Note that the function to compute if a user has permissions to perform the delete action might differ for each entity. For example in our app you can delete a comment if you have edit permissions on the entity but you need owner permissions to delete a group.
We could therefore use this to refactor the
EntityActionDelete component we introduced in section 3 to:
We now only need to pass an
entity prop, which is an instance of a Group or a Post class (fetched from the api). This will give us direct access to the
Also, because of the
canDelete getter, we can now protect against users without the right permission level and return null in the render method. So we don’t have to worry about accidentally rendering something that shouldn’t be seen by the user.
This whole technique comes with great
Typescript support. We can limit the types of entities we can pass to the component (e.g. only
Post ) to only entities that are deletable in the app. Also we get the assurance that both these entities have declared the
canDelete getter otherwise typescript would give us a warning.
In a large React app where re-usability is high and components tend to be generalized this technique proves very helpful because you can customize the behaviour of each entity in its respective class, without changing anything in the component. It is very scalable, there is good separation of concerns, easily testable and we get great typescript support.
7+. More best practices
You can find the second part of this article in the link below: