Menu
|

5 React Architecture Best Practices for 2021

5 React Architecture Best Practices for 2021

5 React Architecture Best Practices for 2021 – SitePoint

Skip to main content

There can be no doubt that React has revolutionized the way we build user interfaces. It’s easy to learn and greatly facilitates creating reusable components that offer your site a consistent look and feel.

However, as React only takes care of the view layer of an application, it doesn’t enforce any specific architecture (such as MVC or MVVM). This can make it difficult to keep your codebase organized as your React project grows.

At 9elements, one of our flagship products is PhotoEditorSDK — a fully customizable photo editor that easily integrates into your HTML5, iOS or Android app. PhotoEditorSDK is a large-scale React app aimed at developers. It requires high performance, small builds, and needs to be very flexible with regards to styling and especially theming.

Throughout the many iterations of PhotoEditorSDK, my team and I have picked up a number of best practices for organizing a large React app, some of which we’d like to share with you in this article.

1. Directory Layout

Originally, the styling and the code for our components were separated. All styles lived in a shared CSS file (we use SCSS for preprocessing). The actual component (in this case FilterSlider), was decoupled from the styles:

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

Over multiple refactorings, we found that this approach didn’t scale very well. In the future, our components would need to be shared between multiple internal projects, like the SDK and an experimental text tool we’re currently developing. So we switched to a component-centric file layout:

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

The idea was that all the code that belongs to a component (such as JavaScript, CSS, assets, tests) is located in a single folder. This makes it very easy to extract the code into an npm module or, in case you’re in a hurry, to simply share the folder with another project.

Importing components

One of the drawbacks of this directory structure is that importing components requires you to import the fully qualified path, like so:

import FilterSlider from 'components/FilterSlider/FilterSlider'

But what we’d really like to write is this:

import FilterSlider from 'components/FilterSlider'

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

export { default } from './FilterSlider';

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

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

And within package.json, we use the main property to set our entry point to the component, like so:

{ "main": "FilterSlider.jsx"
}

With that addition, we can import a component like this:

import FilterSlider from 'components/FilterSlider'

2. CSS in JavaScript

Styling, and especially theming, has always been a bit of a problem. As mentioned above, in our first iteration of the app we had a big CSS (SCSS) file, in which all of our classes lived. To avoid name collisions, we used a global prefix and followed the BEM conventions to craft CSS rule names. When our application grew, this approach didn’t scale very well, so we searched for a replacement. First we evaluated CSS modules, but at that time they had some performance issues. Also, extracting the CSS via webpack’s Extract Text plugin didn’t work that well (although it should be OK at the time of writing). Additionally, this approach created a heavy dependency on webpack and made testing quite difficult.

Next, we evaluated some of the other CSS-in-JS solutions that had recently arrived on the scene:

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

  • Do you need the library to spit out a compiled CSS file for production? EmotionJS and Linaria can do that! Linaria even doesn’t require a runtime. It maps props to CSS via CSS variables, which rules out IE11 support — but who needs IE11 anyways?
  • Does it need to run on the server? That’s no problem for recent versions of all libraries!

For the directory structure we like to put all the styles in a styles.js:

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

This way, pure front-end folks are also able to edit some styles without dealing with React, but they have to learn minimal JavaScript and how to map props to CSS attributes:

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

It’s a good practice to declutter your main component file from HTML.

Striving for the Single Responsibility of React Components

When you develop highly abstract UI components, it’s sometimes hard to separate the concerns. At some points, your component will need a certain domain logic from your model, and then things get messy. In the following sections, we’d like to show you certain methods for DRYing up your components. The following techniques overlap in functionality, and choosing the right one for your architecture is more a preference in style rather than based on hard facts. But let me introduce the use cases first:

  • We had to introduce a mechanism to deal with components that are context-aware of the logged-in user.
  • We had to render a table with multiple collapsible <tbody> elements.
  • We had to display different components depending on different states.

In the following section, I’ll show different solutions for the problems described above.

3. Custom Hooks

Sometimes you have to ensure that a React component is only displayed when a user has logged in to your application. Initially, you’ll do some sanity checks while rendering until you discover that you’re repeating yourself a lot. On your mission to DRY up that code, you’ll sooner or later have to write custom hooks. Don’t be afraid: it’s not that hard. Take a 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(); useEffect(() => { if (auth.user === false) { history.push(redirectUrl); } }, [auth, history]); return auth;
}

The useRequireAuth hook will check if a user is logged in and otherwise redirect to a different page. The logic in the useAuth hook can be provided via context or a state management system like MobX or Redux.

4. Function as Children

Creating a collapsible table row is not a very straightforward task. How do you render the collapse button? How will we display the children when the table isn’t collapsed? I know that with JSX 2.0 things have become much easier, as you can return an array instead of a single tag, but I’ll expand on this example, as it illustrates a good use case for the function as children pattern. Imagine the following table:

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

And a collapsible 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’d use this component in the following way:

<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 a function as children, which gets called in parent component. You might also have seen this technique referred to as a “render callback” or, in special cases, as a “render prop”.

5. Render Props

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

The following code tries to generalize how to fetch data from an API. (Please note that this example is just for demonstration purposes. In real projects, you’d even abstract this fetch logic into a useFetch hook to decouple it even further 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.sitepoint.com); return render(state);
}

As you can see, there’s a property called render, which is a function called during the rendering process. The function called inside it gets the complete state as its parameter, and returns JSX. Now look at 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 destructured from the state object and can be used to drive the response of the JSX. In this case, as long as the promise hasn’t been fulfilled, a “Loading” headline is shown. It’s up to you which parts of the state you pass to the render prop and how you use them in your user interface. Overall, it’s a very powerful mechanism to extract common UI behavior. The function as children pattern described above is basically the same pattern where the property is children.

Protip: Since the render prop pattern is a generalization of the function as children pattern, there’s nothing to stop you from having multiple render props on one component. For example, a Table component could get a render prop for the header and then another one for the body.

Let’s Keep the Discussion Going

I hope you enjoyed this post about architectural React patterns. If you’re missing something in this article (there are definitely more best practices), or if you’d just like to get in touch, please ping me on Twitter.

PS: 9elements is always searching for talented developers, so feel free to apply.

0 Comments Leave a reply

    Leave a comment

    Your comment(click button to send)

    JADA
    Share

    This is a unique website which will require a more modern browser to work!

    Please upgrade today!

    Open chat