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
Table of Contents
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:
- Styled Components: the most popular choice with the biggest community
- EmotionJS: the hot competitor
- Linaria: the zero runtime solution
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.
Continue reading 5 React Architecture Best Practices for 2021 on SitePoint.