React best practices-React at scale (Part 2)

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.

This article follows from Part 1, which can be found here:

https://onoufriosm.medium.com/react-best-practices-react-at-scale-part-1-74234290ef19

7. Folder structure

Coming up with a good folder structure is a difficult task. Mainly because it involves choosing the right naming conventions. There are multiple ways to structure folders for a react app. Some will work for your project and some not. This will be up to you to decide.

Having said that, there are some tips that we can follow to make our folder structure more robust:

  1. Naming should be consistent and predictable. To achieve this similar components should follow the same folder path pattern. For example, a component that shows a form to create a postand a component that shows a form to create a comment could live under /components/Post/Form/Create and /components/Comment/Form/Create respectively. Similarly if every page in our app has a component we render on the left side we could have a pattern that would look like: /components/Page/Home/Left and /components/Page/Groups/Left. Essentially whenever we want to create a new component we should know where to create it based on a well defined pattern.
  2. Relevant files should be kept under a single folder. The component, test, styles, types (for typescript) should all be kept in one folder. This scales very well because if you want to add another file e.g. stories.tsx(if you are using storybook) you can just add it in the same directory especially if done incrementally. Also, having everything centralized in one folder makes it very easy to move the folder and all its assets around or delete it.
  3. Core UI components such as Button, Form, List e.t.c (your UI library essentially) should be kept in a separate directory. This is a nice separation and makes it easy to act on this directory in isolation e.g. some developers can focus on this directory only. Furthermore, we could package it for use in another project or publish all these components as part of Storybook on github pages for the design’s team benefit.
  4. Keep different domains of business logic in different directories. For example if we have a redux store this should be kept in a different directory than our components directory. If we have services such as a service to do our http requests, a service to do currency conversion e.t.c these should be kept in a different directory probably named services. A good way to figure out how to break your code logic is to ask yourself: Can I delete a whole directory and replace it with something else without affecting the rest of my app. If the answer is yes then you have a clear separation of concerns. (e.g. throw away the directory that holds all the logic for Redux and replace it with Apollo)

All the rules above help minimize the decisions that need to be taken when developing a new features.

This is an example folder structure:

Tip: Another pattern I like is to follow similar patterns in the components directory as with the core (UI) directory. For example the component in components/Comment/List will be expected to use the component in core/List .

8. Typescript

I strongly recommend the use of Typescript. Some of the great benefits of typescript are:

  • It’s very helpful during refactoring as it will point out all the components that use the wrong props after we’ve changed the type definitions.
  • Autocomplete. Especially with VSCode integration we get awesome autocomplete making development much more productive.
  • It’s easy to adopt incrementally.

Once you adopt Typescript it is very helpful to learn how to use Utility Types (https://www.typescriptlang.org/docs/handbook/utility-types.html) so that while your React projects grows and evolves you don’t need to re-type interfaces for every component. In cases where you leverage spreading props you would only have to update the interface of the most nested component.

Here’s an example of how we would utilize Pick and Exclude :

In this example ImageModal component renders a Button component, which when clicked will show an image in a modal. The ImageModal receives two props the src for the image and buttonProps to be spread in the rendered Button prop.

Note that we have typed the ImageModal in a way that the buttonProps passed can include any prop of the IButtonProps interface except the children and onClick which are already handled by the ImageModal ; when using the ImageModal you can pass any of the fontSize and color for the buttonProps. The main advantage of this is that if we decide to add any props to the button (e.g. we want to add an icon prop, which if passed can render an icon inside the button) we need to only add it in IButtonProps interface and it will propagate to the buttonProps prop of the ImageModal . This is important as we only require a single change in one interface to propagate all the way up the hierarchy of components (This is especially helpful when a component like Button is deeply nested. We avoid having to add types in multiple other components).

9. Testing

Testing is essential to achieve stability at scale. We’ve all been in that scenario where we have to demo our app and we are filled with stress, praying to the demo gods that nothing goes wrong. Well it doesn’t have to be like that and we can make sure that our app behaves the way it was intended to with extensive testing.

A good rule of thumb is to follow the testing pyramid below to figure out how many of each type of tests we should write.

Unit

This is the type of testing where we test individual (usually small and specific) units of software.

The advice here is to write lots of unit tests. They need the minimum amount of time to write and they are fast to run, so you are able to test all the different combination of states and actions. Aim for over 80% code coverage.

If we follow best practices from previous sections, most unit tests in the app would just be testing that the correct components are rendered and the correct props are passed.

This means that unit tests add minimal overhead and maintaining near 100% of code coverage shouldn’t be too difficult.

You can find more about how to unit test here: https://itnext.io/how-to-unit-test-in-react-72e911e2b8d

Integration

This is the type of testing where we test a combination of several units of software. For example we could test the complete journey of a redux action through the middleware and the expected output after it passes through a reducer. Or we could test how several components work together for example a Form component that uses several other components for its fields.

Again these tests should be relatively fast to run but will take longer to debug. Try to write integration tests at minimum for parts of your application that you (re)use heavily and are critical to your application.

E2E

This is the type of testing that we do in a browser. This will normally involve both the frontend and the backend and we would use libraries such as Cypress or any Selenium based ones. In this type of testing we should test scenarios that our users will go through in their daily activities. Some examples are:

  • ‘As a user I want to sign up.’
  • ‘As a user I want to reply to a post’
  • ‘As a user I want to join a group’

These are slow tests but an essential part of a robust applications. Ideally we should split E2E tests in different categories such as critical (The ones that need to pass before we push a new version of our app in production) and non-critical (We could afford some failures here).

Note: When setting up your E2E test suite you should break down your code in a way that mimics React components. A lot of E2E libraries want us to think our app in pages but we should think of it in components. For example if you have a form component in your app that you reuse all the time you should have file / folder in your E2E directory to perform actions such as fill the form, submit the form and make assertions. Then this can be re-used in any place of the app you use forms.

Bringing it all together

Imagine someone tasked you to build a feed similar to facebook. You would have to query a (paginated) list of posts and display them as a list of cards. In the card there should be an action called “comment”, which once clicked should load the comments for that post and show a form to create a new comment.

Let’s see how we can leverage the above best practices to deliver a feature like this without making any new decisions and without adding complexity to the project.

Write classes

Following from section 6 of the best practices we can add the following classes:

Here we are declaring three classes, namely User, Post and Comment. These all extend the common Entity class, which holds common fields such as id. Also, for the Comment class we are declaring the fields that can be used in the Form component with details for the validation (e.g. max of 255 characters) and placeholder text.

Note: In our app when we declare these classes the app automatically allows you to perform rest api calls to create , update , delete , read for these entities and also adds the necessary reducers in Redux to store the fetched data. This is a very convenient way to introduce new entities to the system.

Write components

The following code snippet shows the component to load the posts for the logged user:

Few things to note here:

a. Fetch is a component that will query the posts for the logged user and show a Placeholder component while the query is being executed.

b. There is no information about how the query is being done (This could be using a rest api or graphql) and there is no information about how the (fallback) placeholder is being shown (This could be using React.Suspense or any other method). All these are abstracted away in nested components that we need not to worry about and can be easily refactored without affecting this component.

c. Constants are being used everywhere instead of hardcoding values. For the List component for example we chose to show a relaxed spacing meaning the distance between the PostCard components will be bigger than the default.

d. The naming convention of the files follows a very easy to understand convention. For example the PostCard lives under src/Post/Card and as you will see later this component renders the coreCard component, which lives under src/core/Card.

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

The following code snippet shows the PostCard component, which will be the card that shows info about the author and comments.

(The following code is shown as one big component. In reality it would be a combination of smaller components.)

Few things to note here:

a. We make use of EntityActionUpdate component to update the post is_liked field.

b. We make use of EntityFormCreate to show a form to create an entity (as suggested in section 3). The form will show the fields passed in the props; in this case the body field of the Comment as defined in its class. The Form component has been programmed to send a POST request on submission with a payload which is the combination of the body prop and the fields inputs.

Write tests

We should write a unit test for each component. This would be very easy as almost all the components created are just calling other components and passing props. The only thing needed is to test that the correct components are rendered and the correct props are passed. Additionally we should test that toggle works as expected to show the comments.

We should also write a few e2e tests to make sure that the posts are loaded correctly, we can create, delete comments and like a post.

Conclusion

I hope you got something out of this article that will help you speed up your development and make your application more robust. We saw how setting up the tooling, structuring the components and abstracting state management can help us a lot in order to achieve that.

Most of our development work should be very easy mentally, without requiring us to make any decisions, that will inevitably increase the complexity of our project. This should hold true even for large pieces of work as we’ve seen with building a Facebook feed clone. Once our app has established some best practices and we have put in place reusable components a feature like the above shouldn’t take more than a couple of days to build, test and deliver.

--

--