5 React Architecture Best Practices — SitePoint

April 25, 2018 0 Comments

5 React Architecture Best Practices — SitePoint



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 code base organized as your React project grows.

Here at 9elements (where I’m CEO), 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 experienced that this approach didn’t scale very well. In the future, our components 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 (e.g. 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' 

The naive approach to solve this problem is to change the component file to index.js:

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

Unfortunately, when debugging React components in Chrome and an error occurs, the debugger will show you a lot of index.js files, and that made this option a no-go.

Another approach we tried was the directory-named-webpack-plugin. This plugin creates a little rule in the webpack resolver to look for a JavaScript or JSX file with the same name as the directory from which it’s being imported. The drawback of this approach is vendor lock-in to webpack. That’s serious, because Rollup is a bit better for bundling libraries. Also, updating to recent webpack versions was always a struggle.

The solution we ended up with 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 (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:

  1. Do you need the library to spit out a compiled CSS file for production? EmotionJS can do that!
  2. Do you have complicated theming issues? Styled Components and Glamorous are your friends!
  3. Does it need to run on the server? That’s no problem for recent versions of all libraries!

For the PhotoEditorSDK we actually ended up creating Adonis — our own CSS-in-JS solution. Feel free to check out the project and assess whether it meets your needs, but to be honest we can’t recommend this approach to everyone. For most other projects we normally go with Styled Components, since it’s really powerful and has a strong community behind it. This is really helpful, especially when you have really complex theming issues. Community plugins like styled-theming are an invaluable resource.

Protip: When styling a lot of HTML tags like this:

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

we create an Atoms.jsx file where we put all the styled components:

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

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

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. Higher-order Components (HOCs)

Sometimes you have to ensure that a React component is only displayed when a user has logged into your application. Initially, you’ll do some sanity checks in your render method until you discover that you’re repeating yourself a lot. On your mission to DRY up that code, you’ll sooner or later find higher-order components that help you to extract and abstract certain concerns of a component. In terms of software development, higher-order components are a form of the decorator pattern. A higher-order component (HOC) is basically a function that receives a React component as parameter and returns another React component. Take a look at the following example:

import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { push } from 'react-router-redux'; export default function requiresAuth(WrappedComponent) { class AuthenticatedComponent extends Component { static propTypes = { user: PropTypes.object, dispatch: PropTypes.func.isRequired }; componentDidMount() { this._checkAndRedirect(); } componentDidUpdate() { this._checkAndRedirect(); } _checkAndRedirect() { const { dispatch, user } = this.props; if (!user) { dispatch(push('/signin')); } } render() { return ( <div className="authenticated"> { this.props.user ? <WrappedComponent {...this.props} /> : null } </div> ); } } const wrappedComponentName = WrappedComponent.displayName || WrappedComponent.name || 'Component'; AuthenticatedComponent.displayName = `Authenticated(${wrappedComponentName})`; const mapStateToProps = (state) => { return { user: state.account.user }; }; return connect(mapStateToProps)(AuthenticatedComponent); } 

The requiresAuth function receives a component (WrappedComponent) as a parameter, which will be decorated with the desired functionality. Inside that function, the AuthenticatedComponent class renders that component and adds the functionality to check if a user is present, otherwise redirecting to the sign-in page. Finally, this component is connected to a Redux store and returned. Redux is helpful, in this example, but not absolutely necessary.

ProTip: It’s nice to set the displayName of the component to something like functionality(originalcomponentName) so that, when you have a lot of decorated components, you can easily distinguish between them in the debugger.

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 is not 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:

import React, { Component } from "react"; export default class Table extends Component { render() { return ( <table> <thead> <tr> <th>Just a table</th> </tr> </thead> {this.props.children} </table> ); } } 

And a collapsible table body:

import React, { Component } from "react"; export default class CollapsibleTableBody extends Component { constructor(props) { super(props); this.state = { collapsed: false }; } toggleCollapse = () => { this.setState({ collapsed: !this.state.collapsed }); }; render() { return ( <tbody> {this.props.children(this.state.collapsed, this.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 the component’s render function. 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 to pass a React component within a callable function as a property and call this function within the render function.

Look at this code, which is trying to generalize how to fetch data from an API:

import React, { Component } from 'react'; import PropTypes from 'prop-types'; export default class Fetch extends Component { static propTypes = { render: PropTypes.func.isRequired, url: PropTypes.string.isRequired, }; state = { data: {}, isLoading: false, }; _fetch = async () => { const res = await fetch(this.props.url); const json = await res.json(); this.setState({ data: json, isLoading: false, }); } componentDidMount() { this.setState({ isLoading: true }, this._fetch); } render() { return this.props.render(this.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 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.

And if you ever consider implementing photo editing into your web application, please check out our product. The PhotoEditorSDK is a powerful and multifaceted tool that enables you to provide your users with high-performance photo editing capabilities. We’ve poured developer years into building the best solution out there, and we’re pretty proud of what we’ve achieved.

Tag cloud