5 React Architecture Best Practices for 2021

There is no doubt that React has revolutionized the way we build user interfaces. It is easy to learn and greatly facilitates the creation of reusable components to provide a consistent look to your website.

However, because React is only responsible for the view layer of your application, it does not implement any specific architecture (such as MVC or MVVM). This can be difficult to keep your codebase in order as your React project grows.

One of our flagship products on 9elements is PhotoEditorSDK – a fully customizable photo editor that can be easily integrated into your HTML5, iOS or Android application. photoEditorSDK is geared towards developers of large React applications. It needs to be high performance, compact in appearance and very flexible in terms of styles (especially themes).

Over the course of many iterations of PhotoEditorSDK, my team and I have mastered many best practices for organizing large React applications, and in this article, we want to share some of those best practices with you.

Catalog layout

Initially, the component styles and code were separate. All styles are stored in a shared CSS file (we use SCSS for pre-processing). The actual component (in this case FilterSlider) has been separated from the styles.

├── components
│   └── FilterSlider
│       ├──  __tests__
│       │   └── FilterSlider-test.js
│       └── FilterSlider.jsx
└── styles
    └── photo-editor-sdk.scss

In several refactorings, we found that this approach does not scale very well. In the future, our components will need to be shared between multiple internal projects, such as the SDK and the experimental text tool we are currently developing. Therefore, we switched to a component-centric file layout.

components
    └── FilterSlider
        ├── __tests__
        │   └── FilterSlider-test.js
        ├── FilterSlider.jsx
        └── FilterSlider.scss

The idea is that all code belonging to components (e.g. JavaScript, CSS, assets, tests) are located in a single folder. This makes it easy to extract the code into the npm module, or if you are in a hurry to simply share that folder with another project.

Importing Components

One of the disadvantages of this directory structure is that importing components requires you to import fully qualified paths, as shown below.

import FilterSlider from 'components/FilterSlider/FilterSlider'

But what we really want to write is this.

import FilterSlider from 'components/FilterSlider'

To solve this problem, you can create an index.js and export the default values immediately.

export { default } from './FilterSlider';

The other solution is a bit more extensive, but it uses the Node.js standard parsing mechanism, making it rock-solid and future-proof. All we have to do is package.json to add a file to the file structure.

components
    └── FilterSlider
        ├── __tests__
        │   └── FilterSlider-test.js
        ├── FilterSlider.jsx
        ├── FilterSlider.scss
        └── package.json

Inside package.json, we use the main attribute to set our entry point to the component, as follows.

{"main": "FilterSlider.jsx"}

With this addition, we can import a component like this one.

import FilterSlider from 'components/FilterSlider'

CSS in JavaScript

Styles, and especially themes, have always been a problem. As mentioned above, in the first iteration of the application, we had a large CSS (SCSS) file in which all classes existed. To avoid name conflicts, we used a global prefix and followed the BEM convention for CSS rule names. This approach did not scale very well when our application grew, so we looked for alternatives. First, we evaluated CSS modules, but at the time they had some performance issues. Also, extracting CSS via webpack’s Extract Text plugin did not work well (although it should be possible at the time of writing). In addition, this approach relied heavily on webpack and made testing very difficult.

Next, we evaluated some other CSS-in-JS solutions that have emerged recently.

  • Styled Components: The most popular choice in the largest community
  • EmotionJS: the hottest competitor
  • Linaria: zero runtime solution

Choosing one of these libraries depends heavily on your use case.

  • Do you need the library to spit out compiled CSS files for production? EmotionJS and Linaria can do it! It maps props to CSS via CSS variables, and CSS variables exclude IE11 support – but who still needs IE11?
  • Does it need to be running on the server? This is no problem for the latest versions of all libraries!

For the directory structure, we want to put all styles in styles.js

export const Section = styled.section`
  padding: 4em;
  background: papayawhip;`;

This way, pure front-end people can also edit certain styles without having to deal with React, but they have to learn a minimum of JavaScript and how to map props to CSS properties.

components
    └── FilterSlider
        ├── __tests__
        │   └── FilterSlider-test.js
        ├── styles.js
        ├── FilterSlider.jsx
        └── index.js

It is a good habit to organize your main component files from HTML.

React components

When you are developing highly abstract UI components, it is sometimes difficult to separate concerns. At some point, your component will need domain-specific logic in the model, and then things get messy. In the following sections, we will show you some of the methods used to dry components. The following techniques overlap in function and the selection of the right technique for the architecture is more style-based than fact-based. But let me start by presenting the use cases.

  • We must introduce a mechanism to handle components that are context-aware to the logged-in user.
  • We must render a table with multiple collapsible elements.
  • We must display different components depending on their state.

In the following sections I will show different solutions to the above mentioned problems.

Customized hooks

Sometimes you have to make sure that React components are only displayed when the user is logged into your application. Initially, you will perform some integrity checks while rendering until you find yourself repeating it many times. Sooner or later, you will have to write custom hooks on the task of making that code dry. Don’t be afraid: it’s not that hard. Look at the following example.

import { useEffect } from 'react';import { useAuth } from './use-auth-from-context-or-state-management.js';import { useHistory } from 'react-router-dom';function useRequireAuth(redirectUrl = "/signup") {
  const auth = useAuth();
  const history = useHistory();
  // If auth.user is false that means we're not
  // logged in and should redirect.
  useEffect(() => {
    if (auth.user === false) {
      history.push(redirectUrl);
    }
  }, [auth, history]);
  return auth;}

The useRequireAuth hook will check if a user is logged in otherwise redirected to another page. logic in the useAuth hook can be provided through a context or state management system (e.g. MobX or Redux).

Function as Children

Creating collapsible table rows is not a very simple task. How do you render the collapse button? How will we display the children when the table is not collapsed? I know it’s much easier with JSX 2.0 because you can return arrays instead of individual labels, but I’ll expand on this example because it illustrates a good use case for functionality as a subpattern. Imagine the following table.

export default function Table({ children }) {
  return (
    <table>
      <thead>
        <tr>
          <th>Just a table</th>
        </tr>
      </thead>
      {children}
    </table>
  );}

and a foldable table body.

import { useState } from 'react';export default function CollapsibleTableBody({ children }) {
  const [collapsed, setCollapsed] = useState(false);
  const toggleCollapse = () => {
    setCollapsed(!collapsed);
  };
  return (
    <tbody>
      {children(collapsed, toggleCollapse)}
    </tbody>
  );}

You can use this component in the following ways.

<Table>
  <CollapsibleTableBody>
    {(collapsed, toggleCollapse) => {
      if (collapsed) {
        return (
          <tr>
            <td>
              <button onClick={toggleCollapse}>Open</button>
            </td>
          </tr>
        );
      } else {
        return (
          <tr>
            <td>
              <button onClick={toggleCollapse}>Closed</button>
            </td>
            <td>CollapsedContent</td>
          </tr>
        );
      }
    }}
  </CollapsibleTableBody></Table>

You simply pass the function as a child to be called in the parent component. You may also have called this technique “render callbacks” or, in special cases, “render props”.

Render Props

The term “Render Props” was coined by Michael, who suggested that the higher-level component pattern could be replaced 100% of the time with regular components with “render props”. The basic idea here is that all React components are functions, and functions can be passed as props. So why not pass React components through props?

The following code tries to outline how to get data from the API. (Please note that this example is for demonstration purposes only. In a real project, you would even abstract this fetch logic to a useFetch hook to further separate it from the UI.) Here’s the code.

import { useEffect, useState } from "react";export default function Fetch({ render, url }) {
  const [state, setState] = useState({
    data: {},
    isLoading: false
  });
  useEffect(() => {
    setState({ data: {}, isLoading: true });
    const _fetch = async () => {
      const res = await fetch(url);
      const json = await res.json();
      setState({
        data: json,
        isLoading: false,
      });
    }
    _fetch();
  }, https%3A%2F%2Feditor.xxx.com);
  return render(state);}

As you can see, there is a property named render, which is a function called during the rendering process. The function called inside it takes the full state as its argument and returns JSX. now check the following usage.

<Fetch
  url="https://api.github.com/users/imgly/repos"
  render={({ data, isLoading }) => (
    <div>
      <h2>img.ly repos</h2>
      {isLoading && <h2>Loading...</h2>}

      <ul>
        {data.length > 0 && data.map(repo => (
          <li key={repo.id}>
            {repo.full_name}
          </li>
        ))}
      </ul>
    </div>
  )} />

As you can see, the data and isLoading parameters are deconstructed from the state object and can be used to drive the JSX response. In this case, the “Loading” heading will be displayed whenever the promise is not fulfilled. It is up to you to decide which parts of the state are passed to the rendering props and how they are used in the UI. Overall, this is a very powerful mechanism for extracting common UI behavior. The functionality described above as a sub-pattern is essentially the same as the pattern children for this property.

Protip: Since the render prop pattern is a generalization of the function as a subpattern, there is nothing stopping you from having multiple render props on a component. for example, a Table component could get a render prop for the header and then get another render prop for the body.

Summary

I hope you enjoyed this article about the architecture React pattern. If you’re missing something in this article (there are definitely more best practices) or if you’d like to get in touch with us, please leave a comment.

Leave a Reply