React best practices-React at scale (Part 1)

Onoufrios Malikkides
9 min readFeb 14, 2021

--

This article outlines a set of best practices that will help with the development of large react applications.

Background

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:

Translation specific component

Then if we want to translate a piece of text we can use the Translation component:

Same component refactored to use Translation component

Here are the benefits we get from this refactoring:

  1. We managed to abstract away the logic for translation into the Translation component. Now there is a centralized translation component where we can change any UI or business logic if needed. If for example the Translation component 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.
  2. It’s now a lot easier to test files that use translation. Before refactoring we would have to mock the useTranslation somehow 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:

Action Component

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 usingRequireExactlyOne 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 Icon, Button or 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 comment, a post, a group e.t.c. Or we can set as favourite a task, 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 entityId. This 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 Icon, Button e.t.c.

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).

Generic Redux

For people using Redux combined with react-redux 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: entityType , entityId and children. entityType and entityId are used in the mapStateToProps and 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 read function.

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, theuseDeleteEntity 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 useToggle hook:

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 entityType and id .

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 Group or 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:

https://onoufriosm.medium.com/react-best-practices-react-at-scale-part-2-11b69b96f5e

--

--

Onoufrios Malikkides
Onoufrios Malikkides

Written by Onoufrios Malikkides

Software developer, writer, speaker, photographer.

Responses (1)