5 React Architecture Best Practices for 2023
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:
- 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.
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();
// 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 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 Pattern
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.
FAQs About React Architecture
React is a JavaScript library for building user interfaces with a component-based architecture. It employs a virtual DOM to optimize performance by minimizing direct DOM manipulation, and data flows in a one-way direction from parent to child components. JSX is used for describing UI components, and React allows components to manage their own state. Additionally, React offers lifecycle methods for handling component events, and client-side routing can be managed with libraries like React Router in single-page applications.
No, React is not a Model-View-Controller (MVC) framework. React is a library for building user interfaces, and it focuses primarily on the “View” part of the MVC architecture. It provides a way to create user interface components and manage their rendering efficiently. React, while it can be used to create user interfaces, doesn’t prescribe a specific model or controller layer. It can be integrated with other libraries or frameworks to handle these aspects, like Redux for state management or custom JavaScript code for handling application logic (controller). So, React is more focused on the “V” (View) part of the MVC pattern and can be combined with other tools and patterns to implement the full MVC architecture in a web application.
Yes, React is primarily known for its component-based architecture. It encourages developers to break down the user interface into reusable and self-contained components. Each component represents a part of the user interface and can have its own logic and state. This approach promotes modularity, reusability, and maintainability in building web applications. React’s component-based architecture is one of its key features and is central to how developers structure and build user interfaces with React.
Yes, there are several common patterns and recommended ways to structure React projects, although the specific structure can vary depending on the project’s size and complexity. Here is a general recommended project structure for organizing a React application:my-react-app/
├── src/
│ ├── components/
│ │ ├── Component1.js
│ │ ├── Component2.js
│ │ └── …
│ ├── pages/
│ │ ├── Home.js
│ │ ├── About.js
│ │ ├── Contact.js
│ │ └── …
│ ├── assets/
│ │ ├── images/
│ │ ├── styles/
│ │ └── …
│ ├── App.js
│ ├── index.js
│ └── …
├── public/
│ ├── index.html
│ ├── manifest.json
│ └── …
├── package.json
├── package-lock.json
├── README.md
└── …
src: This is where the main source code of your React application resides.
components: Store your reusable UI components in this directory.
pages: Create separate files for each major page or route of your application. Each file can represent a specific view or page of your app.
assets: Place assets like images, stylesheets, fonts, or any other static files here.
App.js: The root component of your application, where you typically define your routes and overall application structure.
index.js: The entry point for your React application, where you render the root component and connect it to the DOM.
public: This directory contains static assets that are publicly accessible.
index.html: The main HTML file that serves as the entry point for your application.
package.json: The file that contains project metadata and dependencies.
README.md: A documentation file for your project.
This is a basic structure, and for larger projects, you may want to further organize your code into subdirectories based on functionality or features. Additionally, you can use tools like Create React App or other custom build setups to scaffold your project structure automatically.
Remember that the specific structure can vary depending on your team’s preferences, project requirements, and the tools and libraries you’re using (such as Redux for state management or React Router for routing). It’s important to keep your project organized and maintainable as it grows.
The architectures of React and React Native exhibit both similarities and differences, primarily stemming from their distinct target platforms and rendering mechanisms.
Platform Target: React is primarily intended for building web applications and user interfaces that run in web browsers. It operates by rendering components to a virtual DOM, which is then reconciled with the actual DOM in web applications. On the other hand, React Native is tailored for mobile application development, targeting iOS and Android platforms. It allows developers to leverage React’s component-based approach to create native mobile user interfaces. React Native achieves a native look and feel by rendering components directly to native user interface elements, such as native buttons and text views.
Rendering: React relies on rendering components to a virtual DOM, a representation of the actual DOM in memory. This virtual DOM optimization minimizes direct manipulation of the real DOM, enhancing performance. In contrast, React Native bypasses the web’s DOM entirely and renders components directly to native UI elements. It uses a bridge to facilitate communication between JavaScript and native code for rendering, resulting in true native performance and appearance.
Components: React offers a set of components that map to HTML elements (e.g., divs, spans, inputs), allowing developers to create custom components. React Native, on the other hand, provides a different set of components (e.g., View, Text, Button) that correspond to native user interface elements. Developers can extend and style these components using a CSS-like styling system adapted for native components.
Styling: Styling in React is typically accomplished using standard CSS, CSS-in-JS solutions, or CSS pre-processors. React Native employs a different styling system that resembles CSS but is customized for native components. Styling is achieved through JavaScript objects, and styles are platform-specific, ensuring a native look and feel on each target platform.
API Access: React can access web APIs directly, which is useful for tasks like making HTTP requests. In contrast, React Native abstracts device-specific APIs using a set of JavaScript modules, allowing developers to interact with native features, access device sensors, and perform network operations in a cross-platform manner.
Third-Party Libraries: React has a vast ecosystem of third-party libraries and components primarily tailored for web development. React Native, in contrast, maintains its own ecosystem of third-party libraries and components customized for mobile app development. These libraries often wrap native functionality and provide mobile-specific solutions.
In summary, while React and React Native both adhere to a component-based architecture and share a similar programming model, their architectural differences arise from their unique rendering engines and components optimized for their respective target platforms—web for React and native mobile for React Native. Understanding these distinctions is crucial for selecting the appropriate tool for your project and transitioning between web and mobile development with React.