It may be hard to believe, but this year React turned eight years old. In the technology landscape, especially on client-side web development, this is quite remarkable. How can a simple library for building UIs be that old and still be this relevant?
The reason is, React not only revolutionized the building of UIs, but it also made functional paradigms for building UIs popular. And even then, React did not stop there. They continued to push innovative concepts forward without breaking the existing codes. As a result, React is stabler, leaner, and faster than ever.
But, the downside of React's ever-evolving nature is that best practices change over time. To harvest some of the newest performance benefits, one needs to carefully study the new additions. And figuring that out is not always easy, sometimes it's not straightforward at all.
In this article, we will take a look at the best practices that apply to React in 2021.
Conventions
To structure your work with React, it makes sense to follow a few conventions. Some conventions are even required for the tooling to work smoothly. For example, if you name your components using camelCase, then the following would not work:
const myComponent = () => <div>Hello World!</div>;
ReactDOM.render(<myComponent />, document.querySelector('#app'));
This is because the standard JSX transformer from Babel (or TypeScript) uses the naming convention to decide whether to pass a string or an identifier to React.
As a result, the transpiled code would look as follows:
const myComponent = () => React.createElement("div", null, "Hello World!");
ReactDOM.render(React.createElement("myComponent", null), document.querySelector('#app'));
This is not what we want. Instead, we can use PascalCase. In this case, the JSX transformer will detect the usage of a custom component and the required reference.
const MyComponent = () => <div>Hello World!</div>;
ReactDOM.render(<MyComponent />, document.querySelector('#app'));
In this case, everything is fine:
ReactDOM.render(React.createElement(MyComponent, null), document.querySelector('#app'));
While other conventions are less strict, they should be still followed. For instance, it makes sense to use quoted string attributes instead of JSX expressions:
// avoid
<input type={'text'} />
// better
<input type="text" />
Likewise, it makes sense to keep the attribute quote style consistent. Most guides will propagate using single-quoted strings in JS expressions, and double-quoted strings for these React props. In the end, it doesn’t matter as long as its usage within the codebase is consistent.
Speaking of conventions and props, these should also follow the standard JS naming convention of using camelCase.
// avoid
const MyComponent = ({ is_valid, Value }) => {
// ...
return null;
};
// better
const MyComponent = ({ isValid, value }) => {
// ...
return null;
};
Additionally, be sure not to misuse the names of the built-in HTML component props (for example, style or className). If using these props, forward them to the respective in-built component. Also, keep them at the original type (for example, for style a CSS style object and for className a string).
// avoid
const MyComponent = ({ style, cssStyle }) => {
if (style === 'dark') {
// ...
}
// ...
return <div style={cssStyle}>...</div>;
};
// better
const MyComponent = ({ kind, style }) => {
if (kind === 'dark') {
// ...
}
// ...
return <div style={style}>...</div>;
};
This makes the intention of the props much clearer and establishes a consistency level that is critical for efficient usage of larger component collections.
Component Separation
One of React's biggest advantages is its ability to easily test and reason about components. However, this is only possible if a component is small and dedicated enough to support that.
Back when React first started gaining popularity, they introduced the concept of a controller and a view component to efficiently structure larger components. Even though today we have dedicated state containers and hooks, it still makes sense to structure and categorize components in some way.
Let's consider the simple example of loading some data:
const MyComponent = () => {
const [data, setData] = React.useState();
React.useEffect(() => {
let active = true;
fetch('...')
.then(res => res.json())
.then(data => active && setData(data))
.catch(err => active && setData(err));
return () => {
active = false;
};
}, []);
return (
data === undefined ?
<div>Loading ...</div> :
data instanceof Error ?
<div>Error!</div> :
<div>Loaded! Do something with data...</div>
);
};
Of course, a componentless action would be better suited here. But the point is that the written component has to both gather the data and display it.
A cleaner model would imply a separation that could look like this:
const MyComponent = ({ error, loading, data }) => {
return (
loading ?
<div>Loading ...</div> :
error ?
<div>Error!</div> :
<div>Loaded! Do something with data...</div>
);
};
const MyLoader = () => {
const [data, setData] = React.useState();
React.useEffect(() => {
let active = true;
fetch('...')
.then(res => res.json())
.then(data => active && setData(data))
.catch(err => active && setData(err));
return () => {
active = false;
};
}, []);
const isError = data instanceof Error;
return (
<MyComponent
error={isError ? data : undefined}
loading={data === undefined}
data={!isError ? data : undefined} />
);
};
To further improve it, the most ideal separation is extraction into a custom hook:
function useRemoteData() {
const [data, setData] = React.useState();
React.useEffect(() => {
let active = true;
fetch('...')
.then(res => res.json())
.then(data => active && setData(data))
.catch(err => active && setData(err));
return () => {
active = false;
};
}, []);
const isError = data instanceof Error;
return [data === undefined, !isError ? data : undefined, isError ? data : undefined];
}
const MyComponent = () => {
const [loading, data, error] = useRemoteData();
return (
loading ?
<div>Loading ...</div> :
error ?
<div>Error!</div> :
<div>Loaded! Do something with data...</div>
);
};
Hooks
React hooks are among the most debated technology features in the frontend space. When they were first introduced, they were considered elegant and innovative. On the flip side, there have been a growing number of critics over the years.
Pros and cons aside, in general, using hooks can be a best practice depending on the scenario.
Keep in mind that some hooks are there to help you with performance optimizations:
- useMemo helps avoid doing expensive calculations on every re-render.
- useCallback produces stable handlers, similarly to useMemo, but more conveniently geared towards callbacks.
As an example, let’s look at the following code without useMemo:
const MyComponent = ({ items, region }) => {
const taxedItems = items.map(item => ({
...item,
tax: getTax(item, region),
}));
return (
<>
{taxedItems.map(item => <li key={item.id}>
Tax: {item.tax}
</li>)}
</>
);
};
Considering there might be a lot of items in that array, and that the getTax operation is quite expensive (no pun intended), you’d have quite a bad re-rendering time, assuming minimal items and region change.
Therefore, the code would benefit a lot from useMemo:
const MyComponent = ({ items, region }) => {
const taxedItems = React.useMemo(() => items.map(item => ({
...item,
tax: getTax(item, region),
})), [items, region]);
return (
<>
{taxedItems.map(item => <li key={item.id}>
Tax: {item.tax}
</li>)}
</>
);
};
The beauty of useMemo is that it's almost invisible. As you can see, all we need to do is to wrap the computation in a function. That's it. No other changes required.
A more subtle issue is the lack of useCallback. Let's have a look at some very generic code:
const MyComponent = () => {
const save = () => {
// some computation
};
return <OtherComponent onSave={save} />;
};
Now, we don't know anything about OtherComponent, but there are certain possible changes originating here, for example:
- It’s a pure component and will prevent re-rendering, as long as all props remain untouched.
- It uses the callback on either some memoization or effect hooks.
- It passes the callback to some component that uses one of these properties.
Either way, passing values as props that essentially have not changed should also result in values that have not changed. The fact that we have a function declared inside our rendering function will be problematic.
An easy way out is to write the same thing using useCallback:
const MyComponent = () => {
const save = React.useCallback(() => {
// some computation
}, []);
return <OtherComponent onSave={save} />;
};
Now, the recomputed callback is taken only if one of the dependencies given in the array changed. Otherwise, the previous callback (for instance, a stable reference) is returned.
Like before, there are almost no code changes required for this optimization. As a result, you should always wrap callbacks using useCallback.
Components
Speaking of pure components, while class components had the PureComponent abstraction, a functional pure component can be introduced to React explicitly using memo.
// no memoed component
const MyComponent = ({ isValid }) => (
<div style=\{{ color: isValid ? 'green' : 'red' }}>
status
</div>
);
// memoed component
const MyComponent = React.memo(({ isValid }) => (
<div style=\{{ color: isValid ? 'green' : 'red' }}>
status
</div>
));
The React documentation is quite detailed about memo. It says: “If your component renders the same result given the same props, you can wrap it in a call to React.memo for a performance boost in some cases by memoizing the result. This means that React will skip rendering the component, and reuse the last rendered result.”
Keep in mind that — like any other comparison done by React — the props are only shallowly compared. Therefore, this optimization is only applied if we are careful what to pass in. For instance, if we use useMemo and other techniques for complex props such as arrays, objects, and functions.
You may have noticed that we exclusively used functional components. As a matter of fact, since the introduction of hooks, you can practically work without class components.
There are only two possible reasons to still use class components:
- You want to have access to the more sophisticated life cycle events. For example, shouldComponentUpdate.
- You want to introduce error boundaries.
However, even in these cases, you might just need to write one React class component to fulfill your needs. Look at this boundary:
export class Boundary extends React.Component {
state = {
error: undefined,
};
componentDidCatch(error) {
this.setState({
error,
});
}
render() {
const { error } = this.state;
const { children, ShowError } = this.props;
if (error) {
return <ShowError error={error} />;
}
return children;
}
}
Not only will the component catch any errors which may appear in its children, but it will also display a fallback component passed in as ShowError receiving a single prop: the error.
Operators
Some operators can be used to simplify the tree construction in React. For instance, the ternary operator allows us to write code that looks like this:
<div>
{currentUser ? <strong>{currentUser}</strong> : <span>Not logged in</span>}
</div>
Boolean operators such as && and || may also be useful, but there are a few traps to watch out for. As an example, look at this code snippet:
<div>
{numUsers && <i>There are {numUsers} users logged in.</i>}
</div>
Assuming that numUsers is always a number between 0 and the total number of users, we'd end up with the expected output if numUsers is positive.
<div>
<i>There are 5 users logged in.</i>
</div>
However, for the edge case of zero users, we'd get this:
<div>
0
</div>
Which may not be what we wanted, so a boolean conversion or more explicit comparison could help here. In general, the following is more readable:
<div>
{numUsers > 0 && <i>There are {numUsers} users logged in.</i>}
</div>
Now, in the zero users edge case scenario we get:
<div>
</div>
Using the ternary operator as an exclusive boolean operator avoids the issue completely. But what about a state where we don't want to render anything? We could either use false or an empty fragment:
<div>
{numUsers ? <i>There are {numUsers} users logged in.</i> : <></>}
</div>
The empty fragment has the advantage of giving us the ability to just add content later. However, for users less familiar with React, it could look a bit strange.
Conclusion
In this article, we went over some of the best practices that make your React codebase easier to work with. By switching over from class components to functional components, you can dive more into hooks. This will provide the ability to automatically introduce a great separation of concerns, where the behavioral aspects are all done in functions and rendering is defined within components.
By following a set of useful conventions, together with some techniques such as the use of the right operators, hooks, and separation of concerns, you should end up with a clean codebase that can be maintained and extended quite easily.
Top comments (6)
Rather than
kind === "dark"
I would replace it withvariant === "dark"
imoAlso you could use !!numUsers && so it will be falsy for 0.
Great! Thanks for sharing.
In your component separation example, I would have used an AbortController instead of the active flag.
That way you can abort the fetch if the component is unmounted.
About the state management of data fetching, you can just opt-in the react-query
Great article !