According to StackOverflow 2022 developer survey, React was the most wanted framework in 2022 -- the most popular framework for the fifth year in a row. React continues to progress rapidly, and adopting best practices is important to ensure the best outcomes. This article does explore some of these best practices to make the most out of React.
First, let's understand what makes React a tricky library to work with.
React is a minimal view library that differs from full-blown frameworks like Angular. Where Angular, by default, comes equipped with various tooling, architectural patterns, and libraries, React heavily relies on the open-source community for even the most common libraries.
Regarding architecture and following conventions, React is highly flexible and unopinionated. This can leave developers with decision fatigue when structuring their projects, naming files and folders, and optimizing performance.
Without clear guidelines, developers may inadvertently write anti-patterns in their React applications, making them unnecessarily complex, difficult to scale, and low-performing. To avoid these anti-patterns and create a more robust, scalable, and high-performing React application, let's explore some common techniques that can be implemented.
Folder Structure and Naming Conventions
When starting a new React project, I follow a standard directory structure and naming conventions for files and folders.
This saves time later on when the project becomes complex and helps with better architecture. The folders are named in lowercase, with the separation of concerns principle in mind. Pages are grouped in a pages
directory, with each page's UI broken down into components. These components are further broken down based on their granularity. I'm using Atomic Design to organize UI components into atoms, molecules, and organisms.
For instance, a Signup page can be composed of a Signup Form and a Signup Button, with the Signup Form comprising of a Form component, which in turn can have input and label components. Additionally, logic can be organized into utility functions, custom hooks, higher-order components, etc. Each component is also placed in its folder.
Based on the information above, the following is an example of the directory structure that a React project could have:
.
└── src/
├── components/
│ ├── atoms/
│ │ ├── button/
│ │ │ ├── Button.js
│ │ │ └── Button.css
│ │ ├── input
│ │ └── ...
│ ├── molecules/
│ │ ├── form/
│ │ │ ├── Form.js
│ │ │ └── Form.css
│ │ └── ...
│ └── organisms/
│ ├── forms/
│ │ ├── signup-form/
│ │ │ ├── SignupForm.js
│ │ │ └── SignupForm.css
│ │ └── ...
│ └── ...
├── state/
│ ├── actions
│ └── reducers
├── hoc
├── pages/
│ └── Signup/
│ ├── Signup.js
│ └── Signup.css
├── api/
│ ├── constants
│ └── functions/
│ ├── auth.js
│ ├── user.js
│ └── ...
├── hooks/
│ ├── useAuth.js
│ ├── useLocalStorage.js
│ ├── useWindow.js
│ └── ...
└── utils/
├── dates.js
├── formatString.js
└── ...
If you use a state management library like Redux or React's default Context API, you can organize your actions and reducers into separate folders under the state
directory as shown above.
All utility functions and custom hooks are camel-cased. React by default endorses using the “use-” prefix when naming custom hooks. The components folder is also broken down into atoms, molecules, and organisms based on the granularity of the components you build in your React application.
Fragments
Fragments help you avoid unnecessary HTML in your React components. Let's walk through what they are and how we use them.
When creating a new React component, you'll likely start by returning an empty <div>
element, since a React component can only return a single root element.
export default function MyComponent() {
return (
<div>
<h1>This is my component</h1>
</div>
);
}
However, this approach can result in a lot of unnecessary <div>
elements in your code, bloating your templates.
Fortunately, React provides a solution to this problem through Fragments. Fragments allow you to use void HTML elements as wrappers without adding unnecessary elements to your code.
import React from "react";
export default function MyComponent() {
return (
<React.Fragment>
<h1>This is my component</h1>
</React.Fragment>
);
}
Further, you can also directly use an empty HTML element (<>
) as a shorthand notation for a fragment:
import React from "react";
export default function MyComponent() {
return (
<>
<h1>This is my component</h1>
</>
);
}
Composability and Reusability
The next best practice you can adopt when building your React components is making them composable and reusable. Let's understand how.
In the past, I used to create my React projects with a single, large component because I found it too time-consuming to break things down into smaller components. However, I soon realized that as the app grew in complexity, it became even more challenging to refactor and test the code. Additionally, my development speed slowed down as I had to write all components from scratch without reusing them.
Take a look at the following HomePage
component:
import React from "react";
export default function HomePage() {
return (
<>
<div>
<div>
<svg className="gradient">
<path
fill="url(#45de2b6b-92d5-4d68-a6a0-9b9b2abad533)"
fillOpacity=".3"
d="M317.219 518.975L203.852 678 0 438.341l317.219 80.634 204.172-286.402c1.307 132.337 45.083 346.658 209.733 145.248C936.936 126.058 882.053-94.234 1031.02 41.331c119.18 108.451 130.68 295.337 121.53 375.223L855 299l21.173 362.054-558.954-142.079z"
/>
<defs>
<linearGradient
id="45de2b6b-92d5-4d68-a6a0-9b9b2abad533"
x1="1155.49"
x2="-78.208"
y1=".177"
y2="474.645"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#9089FC" />
<stop offset={1} stopColor="#FF80B5" />
</linearGradient>
</defs>
</svg>
</div>
<nav className="navbar">
<div>
<a href="#">
<span>Home</span>
</a>
</div>
<div>
<button type="button">
<span>Open Menu</span>
</button>
</div>
<div className="hidden lg:flex lg:gap-x-12">
<a href="#">Page 1</a>
<a href="#">Page 2</a>
<a href="#">Page 3</a>
<a href="#">Page 4</a>
</div>
<div>
<a href="#">
Log in <span>→</span>
</a>
</div>
</nav>
<main>
<div>
<div>
<div class="banner">
<div>
<p>This is an announcement!</p>
</div>
</div>
<div className="hero">
<h1>Get onboarded today</h1>
<p>lorem ipsum dolor sit amet, consectetur adip</p>
<div className="mt-10 flex items-center justify-center gap-x-6">
<a href="#">
Learn more <span>→</span>
</a>
</div>
</div>
</div>
</div>
</main>
</div>
</>
);
}
As you add more HTML to the HomePage component, it becomes longer and more difficult to refactor, test, and debug. To address this issue, it is better to break down the HomePage
component into smaller, composable components:
import React from "react";
import { Gradient } from "./Gradient";
import { Navbar } from "./Navbar";
import { Hero } from "./Hero";
export default function HomePage() {
return (
<>
<Gradient />
<Navbar />
<Hero />
</>
);
}
This not only makes the code more readable and maintainable but also enables us to build future features of our React application faster. For instance, we can reuse the Navbar component in other parts of the app by passing some props to it.
Session Replay for Developers
Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — an open-source session replay suite for developers. It can be self-hosted in minutes, giving you complete control over your customer data.
Happy debugging! Try using OpenReplay today.
Performance
Why bother building a React application if it doesn't perform well? As your app grows in complexity, it becomes increasingly important to focus on its performance. Performance is influenced by multiple factors, so we'll explore some key factors that impact the performance of your React application, provide examples, and suggest ways to improve it.
Avoid Deeply Nested Components
We’ve seen that when working with larger and more complex components, you need to break them down into smaller components. However, this might develop into a performance anti-pattern where you start nesting too many smaller components inside one big single component.
For instance, let’s look at the following Counter
parent component. It renders two separate components called Increment
and Decrement
:
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
const Increment = () => {
return <button onClick={handleIncrement}>Increment +</button>;
};
const Decrement = () => {
return <button onClick={handleDecrement}>Decrement -</button>;
};
const handleIncrement = () => setCount(count + 1);
const handleDecrement = () => setCount(count - 1);
return (
<div>
<Increment />
Value of Count: {count}
<Decrement />
</div>
);
}
Both Increment
and Decrement
are children component that lives inside the parent Counter
component and use functions defined in the parent. By defining it here, we don't need to provide a prop for that function. It feels very intuitive, and it works perfectly fine. But there's a major problem here. Every time the parent component is rendered it will also redefine the child component, which means it gets a new memory address, and that could lead to performance issues and unpredictable behavior.
The solution is to either not define the children component at all or to move them out of the parent and pass the function in as a prop, as shown below:
import { useState } from "react";
const UpdateCount = ({ handleClick, type }) => {
return (
<button onClick={handleClick}>
{type === "increment" ? <>Increment +</> : <>Decrement -</>}
</button>
);
};
export default function Counter() {
const [count, setCount] = useState(0);
const handleIncrement = () => setCount(count + 1);
const handleDecrement = () => setCount(count - 1);
return (
<div>
<UpdateCount type={"increment"} handleClick={handleIncrement} />
Value of Count: {count}
<UpdateCount type={"decrement"} handleClick={handleDecrement} />
</div>
);
}
This example shows how React may seem simple at first, but if you lack a complete understanding of its inner workings, it's possible to inadvertently cause problems for yourself.
Understand useMemo and useCallback hooks
The useMemo
and useCallback
hooks are the most important hooks used for performance consideration. However, we must understand where and when they should be used.
It's natural to have more than one state in a component and perform computations when those state value change. Consider the following example where we have two pieces of state, regularState
and expensiveState
:
import { useState } from "react";
export default function HeavyComponent() {
const [regularState, setRegularState] = useState();
const [expensiveState, setExpensiveState] = useState();
const expensiveComputation = () => {
console.log(
"This expensive computation will run everytime expensiveState changes"
);
};
const handleExpensiveStateClick = () => {};
const handleRegularStateClick = () => {};
const expensiveComputationResult = expensiveComputation();
return (
<>
<button onClick={handleRegularStateClick}>Mutate regular state</button>
<button onClick={handleExpensiveStateClick}>
Mutate expensive state
</button>
</>
);
}
Let's say we also have a function as in the above snippet, called expensiveComputation
, that performs an expensive calculation whenever the state value of expensiveState
changes. However, with the way the above code is written, the expensiveComputation
function will run every time the component re-renders, and we know that all state changes lead to re-renders of a component. Thus even when you mutate regularState
, the expensiveComputation
function will run. The more pieces of state you have in this component, the more often that function will run, leading to performance issues for your component HeavyComponent
.
To fix this performance bottleneck, you can use the useMemo
hook.
const expensiveComputationResult = useMemo(
expensiveComputation(expensiveState),
[expensiveState]
);
When we wrap the function execution in a useMemo
hook, the function is invoked only when the value of expensiveState
changes.
The useMemo
equivalent for dealing with components re-rendering when a prop value changes is the useCallback
hook. When you wrap a function that mutates the prop of a child component inside a useCallback
hook, the component only re-renders when its underlying prop changes.
Optimize Load Time with Lazy Loading
A common performance nuance you might run into as your app grows big in size and complexity is a bigger bundle size that takes a long time to load. Lazy loading can help you achieve that; let's see how.
Both Next.js and Create-React-app come with default support for code splitting. Code splitting is a technique that allows you to asynchronously import a certain piece of code when it's required.
For instance, you can asynchronously lazy load assets like images, css, etc in your React components:
export default function AsyncImport() {
const greeting = async () => {
const { greetingImage } = await import("./greeting.png");
return greetingImage;
};
return <>...</>;
}
So now only when the greeting
method is invoked the greetingIamge
will be loaded in your application.
React also allows you to lazy load components besides just JavaScript files:
import { Suspense, lazy } from "react";
const LazyLoadedComponent = lazy("./LazyLoadedComponent");
export default function AsyncImport() {
const greeting = async () => {
const { greetingImage } = await import("./greeting.png");
return greetingImage;
};
return (
<>
<Suspense fallback={<p>Loading...</p>}>
<LazyLoadedComponent />
</Suspense>
</>
);
}
You can lazy load the component import and wrap it in a special Suspense
component which will then only import that React component in your application when needed.
Efficient State Management
Managing state in React applications is often a mess, especially for large applications that change quite frequently. Most of the performance bottlenecks in your app will be centered around mismanaged state causing unnecessary re-renders.
You should try to use a good state management library like Redux or React Query which helps you manage global state.
Conclusion
Let's sum up what we've learned today. When beginning a new React project, it's recommended to adopt a modular directory structure and adhere to standard naming conventions. To enhance performance, try to avoid using unnecessary divs and consider using fragments instead. Additionally, aim to break down large components into smaller, reusable ones and keep performance considerations in mind while developing your React components.