You've just started your React app and you already need to make development choices that may later impact the scalability or the performance of your app. So you have no idea what to choose between the thousand different ways of building your react app: let me show you some of the most important paradigms & technical tips you should follow when developing React!
CMPS is simple & intuitive: it stands for Components, Modules, Pages, Services and allows you to drive to the simplest and most effective way to scale your React app. Whatever libraries you use to store your app's state, CMPS integrates seamlessly with the way you will manage pages/views throughout your app. Basically, it looks like this:
src/
... components/
... .......... MyGlobalComponent/
... .......... ................. components/
... modules/
... ....... MyModule/
... pages/
... ..... MyPage/
... ..... ...... components/
... ..... ...... pages/
... services/
... ........ MyService/
Each of the root directories have one purpose, that can be summarized this way:
💡 You can learn more in this article showing you in details one example of an app using CMPS architecture along with react-redux, styled-components, and Material-UI; but you could adapt it to your need easily by following the architecture's definition!
This has been well debated on the web since the introduction of Hooks by React. Here are 3 reasons why you should use Functional components instead of Class components:
Since the introduction of hooks, React comes with a whole bunch of different hooks (not taking into account the independent libraries bringing some more). But many things are good to know not to use them in vain nor when it would lead to very poor performances. Let’s begin with a classic:
useEffect is only performing SameValue (aka Object.is) comparison
Meaning that when you pass an object or an array to useEffect’s dependency list, it will trigger the effect every time the reference of this object (or the array) updates… Which basically means, if the object (or the array) is a prop of the component and you don't memoize it (which you naturally don't), every time the parent component re-renders.
To avoid this behavior, you have 2 options:
import React from 'react';
const MyComponent = ({ object }: Props) => {
const { stringVar, booleanVar } = object;
React.useEffect(() => {
if (booleanVar) {
/* do something with stringVar */
console.log(stringVar);
}
}, [stringVar, booleanVar]);
return <div>/* Awesome UI here */</div>;
}
import { useDeepCompareEffect } from 'use-deep-compare';
const MyComponent = ({ object }: Props) => {
useDeepCompareEffect(() => {
if (object.booleanVar) {
/* do something with object.stringVar */
console.log(object.stringVar);
}
}, [object]);
return <div>/* Awesome UI here */</div>;
}
💡 This reasoning indeed also applies to useCallback and useMemo
useCallback & useMemo are not always improving rendering performances
It is indeed intuitive to think (and it is common to see it on the web) that using useCallback and useMemo will lead to better rendering performances. As a React developer should be aware, this is definitely not true, as covered in this awesome, interactive and complete article on useMemo and useCallback (I highly suggest you take a deep look into this one).
There are only 2 specific cases you would want to use these 2 hooks (see in the article for more details):
import React from 'react';
const MyMemoComponent = React.memo({ object }: Props) => {
return <div style={style}>/* Awesome UI here */</div>;
});
const MySuperComponent = () => {
const memoStyle = React.useMemo(() => ({
display: 'block',
margin: '',
}), []);
return <MyMemoComponent style={memoStyle} />
}
💡 You could create your own HOC to deeply memoize props of your component, preventing useless re-rendering (the way you would check deep equality in componentShouldUpdate in class components):
import React from 'react';
import { dequal } from 'dequal';
const deepMemo =
<Props extends object>(Element: React.ComponentType<Props>) =>
React.memo(Element, dequal);
For the same reason as above, this won't slow down your UI in any way: dequal has been benchmarked to almost 300k+ comparisons/sec.
Let's consider a UI containing 1000 deeply memoized components (which is a huge but reachable number):
At worst, 1 deep comparison takes 1 / 300000 sec; which means 1000 deeply memoized components take 1 / 300 sec to check if they need to re-render. This is 5 times faster than a 60 fps UI!
Use createContext & useContext to prevent prop drilling
Prop drilling is the (bad) habit to pass props to another component down the component tree, through components that do not need them. It raises issues at the intermediary components’ level:
As we saw earlier, avoiding prop drilling is one of state management libraries’ goals. However, there are times you don’t want to store some data in your app’s state because your data is only relative to some view and you don’t need to save or share it across different views — typically form data. In these cases, you need to store it in the highest common parent of consumer components (so that each of the consumers can access the data).
When you face such a situation, you’d rather choose to create a context (I suggest you do that in the parent component file):
export const FormInputContext = React.createContext<string>();
Then, considering your state value is named inputValue
, wrap your parent component with:
<FormInputContext.Provider value={inputValue}>
...
</FormInputContext.Provider>
Finally, in your children (consumer) components, use the context to access the value, a such:
const ConsumerComponent = () => {
const inputValue = React.useContext(FormInputContext);
return (
<input value={inputValue}>
...
</input>
);
}
Be aware that consumer components will re-render every time the context value is updated. If (and only if) your consumer component is expensive to render, you should consider splitting your form values into multiple contexts — as I did in the previous example. This typically isolates re-rendering at the input level.
There are many reasons why you should normalize your app's global state, one of them aiming at isolating re-rendering. However, to achieve this goal, you need to follow some particular patterns in retrieving data from selectors:
import { DefaultRootState } from 'react-redux';
import { createStructuredSelector } from 'reselect';
export const selectMyDataName = (state: DefaultRootState) =>
state.data.name;
export const selectMyDataDescription = (state: DefaultRootState) =>
state.data.description;
export const selectMyData = createStructureSelector({
name: selectMyDataName,
description: selectMyDataDescription,
});
import { dequal } from 'dequal';
import { DefaultRootState, useSelector } from 'react-redux';
export const useMyData = useSelector(
(state: DefaultRootState) => state.data,
dequal
);
The first way is recommended because you have hands on what you want to memoize (and is the recommended way of designing your selectors, as per the redux documentation). Quick maths show us that both ways have the same complexity (we are performing comparisons on the same number of fields: those making up our data).
When developing a React app, you will recognize repeating patterns. This should typically smell like a Dont Repeat Yourself warning to you: if you find yourself rewriting UI logic, you should create a Global Component for it.
But sometimes, it happens at a higher level. Take for example the common situation where you need to fetch data before displaying it, which corresponding logic is highlighted in the following code:
import { mapStateToProps, mapDispatchToProps } from './MyComponent.container';
// isFetching & data is passed through mapStateToProps
interface StateProps = ReturnType<typeof mapStateToProps>;
// fetchData is passed through mapDispatchToProps
interface DispatchProps = typeof mapDispatchToProps;
interface Props extends StateProps, DispatchProps {}
const ConsumerComponent = ({ data, isFetching, fetchData }: Props) => {
useEffect(() => {
fetchData(); // dispatches the action to fetch data
}, [fetchData]);
if (isFetching) return <Loader />; // while we are fetching, display a Loader
return data;
}
Here, you can’t simplify the code just by creating a hook, because you need to act on rendering. Instead, what you need is a HOC that handles this kind of logic for you (and cleans your code, improving its maintainability). Let’s create it:
⏬ Fetch HOC
export const fetch = (
actionCreator: () => Action,
) => <Props extends object>(
Element: React.ComponentType<Props>
) => ({ isFetching, ...props }: Props) => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(actionCreator()); // dispatches the action to fetch data
}, [dispatch]);
if (isFetching) return <Loader />; // displays a Loader while fetching data
return <Element {...props} />;
}
⏬ Container
export default compose(
connect(mapStateToProps, mapDispatchToProps),
fetch(fetchData),
)(ConsumerComponent);
Which now saves you some time, but more importantly avoid repeating pieces of your code! What you’ve just been taught is that when duplicating rendering logic, you should extract and generalize logic into a HOC— not too much: it could cost you simplicity.
💡 If your action creator needs props from the component (like the route's match for example), you could pass as argument a function that takes props as argument and returns actions created accordingly.
Any React app’s bundle size can get large and thus require the user to wait for the download to be complete before using your app. However, this can be monitored and you can take actions to reduce your app’s bundle size.
The first step is to analyze the bundle size: you may be able to find that your app’s weight is made of large dependency libraries, such as lodash or Material-UI. A good way of avoiding this is to import parts of the libraries instead of whole libraries. Because let’s be honest: you never use everything of lodash, recompose or Material-UI.
import _ from 'lodash'; // whole library import
import isEqual from 'lodash/isEqual'; // named import
The second step is to lazy load your React components (I suggest lazy-loading pages at the router level to have the most efficient loading in the fewest changes) using React's lazy:
const MyPageComponent = React.lazy(() => import('./MyPageComponent'));
If you want to reduce your app’s bundle size even further, you may consider switching to Preact, a 5 kb alternative to React. As stated in this article, the switch is as easy as 2 lines of code.
How can I apply all the awesome advice you just gave me?
I created a template repository allowing you to start a React project with all the best tools I presented and some generators to help you develop faster, without losing scalability and robustness.
You are free to star this template repository ✨
This article is the last part of a larger trilogy, which goal is to give you all the best tools, advise you on how to use them and show you all the pitfalls you need to avoid when developing with React.
The first part, showing you a list of useful tools to kickstart your react app, can be read here.
The second part, showing you an example of the CMPS architecture, can be read here.