8 Tips to Master Modern React App Development

13
minutes
Mis à jour le
23/11/2020

Share this post

Start developing your React app using these modern React app development tips to build scalable and maintainable React apps!

#
Front-end
#
Javascript
#
PWA
#
Performance
#
React
#
Typescript

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!

📚 Build your app following the CMPS architecture

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:

  • Components: where you store your app's global components (those used in at least 2 independant places of your app)
  • Modules: where you store each of your app's state entity module (the part of your code which is responsible for managing some part of your app's global state)
  • Pages: where you store each of your independant, unique views (meaning each of the pages is accessible only through a change in the URL)
  • Services: where you store each of your utilities (these parts of your code that help in managing data or triggering some specific behaviors not related to your app's global state)

💡 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!

🔀 Functional components are better than Class components

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:

  • No code you don't own: functional components and class components are equivalent in possibilities (what you can achieve with each one), but functional components lead you to write less code and thus:
    • Lead you to less bugs
    • Lead you to no bug due to some behavior you were not aware of
  • No more this: the this in javascript does not behave exactly the same as in other languages, meaning that any new javascript developer on your project may encounter issues using it. This could easily be argued by seniors: "just don't hire juniors!". However, one particular bug is easily reproducible if you don't know it and any React senior knows that this is a caveat of class components.
  • Code splitting: hooks are now responsible for some part of your component's logic, which allows you to split your code into multiple parts, bringing more reusability. And as you also know, code splitting is one of the most important paradigms to follow for building a scalable and maintainable codebase (see Martin Fowler's Refactoring book for more deep technical details).

🔱 React Hooks: diving into the technical part

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:

  • Recommended: destructure the object and only pass properties of primitive type that you use in the effect (and reiterate if properties are not primitive)
    Note: this can be painful if you have many props to destructure and pass to the dependency list.
    Example:
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>;
}
  • Use useDeepCompareEffect from use-deep-compare library which will perform a deep equal on the dependency list
    Note: you could argue that this hook could slow down your app. You are wrong: with up to 300k+ comparisons/sec on complex objects (cf benchmark of dequal used by use-deep-compare), this can't slow down your UI in any way.
    Example:
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):

  • When you want to compute a computationally expensive value (involving much more than a couple of additions or variable assignments)
  • When you want to prevent useless re-rendering of children components using React.memo HOC
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 do quick maths

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:

  • Every intermediary component have a prop that is not related to its purpose at all: the component’s purpose is therefore no longer intuitive
  • Every intermediary component have a dependency on the component tree, which leads to more rework when we want to generalize it (typically you’ll need to remove this intermediary prop)
  • Every intermediary component will update each time the prop updates, whereas it shouldn’t because it does not use this prop

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.

✅ Memoize your custom selectors to prevent re-rendering

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:

  • At the selector level (recommended): you create a selector aggregating normalized data (of native javascript type) using createStructuredSelector (from reselect, memoizing input selectors using SameValue comparison):
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,
});
  • At the hook level: you bulk denormalize your app's global state (returning a whole new object every time) and isolate re-rendering by memoizing the selector's output:
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).

⏫ Write your very own Higher-Order Components

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.

⏬ Optimize your bundle size by naming your imports

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.