5 best practices for using React hooks in production

The author of the article, the translation of which we are publishing today, says that commercetools adopted React hooks at the beginning of 2019 - at the time they appeared in React 16.8.0. Since then, the company's programmers have been constantly processing their code, translating it into hooks. React hooks allow you to use component state and use other React features without using classes. Using hooks, you can, when working with functional components, “connect” to the events of the component life cycle and respond to changes in their state.



Briefly about the results of implementing hooks


If in a nutshell to talk about what the hooks gave us, it turns out that they helped to simplify the code base by extracting the logic from the components and facilitating the composition of different capabilities. Moreover, the use of hooks has led to the fact that we have learned a lot. For example, to how to better structure your code through continuous improvement of existing functionality. We are sure that we will learn many more interesting things while continuing to work with React hooks.

Strengths of hook implementation


Here's what the introduction of hooks gave us:

  • Improving code readability. This is possible thanks to the use of smaller component trees than before. The creation of such component trees is facilitated by our desire to abandon rendering properties and higher-order components.
  • Improving code debugging capabilities. We had at our disposal an improved visual representation of the code and additional debugging information provided by the React developer tools.
  • Improving the modularity of the project. Due to the functional nature of hooks, the process of creating and applying logic suitable for repeated use has been simplified.
  • Separation of responsibilities. React components are responsible for the appearance of the interface elements, and hooks provide access to the encapsulated program logic.

Now we want to share our recommendations on the use of hooks in production.

1. Timely retrieve hooks from components and create custom hooks


Starting to use hooks in functional components is pretty easy. For example, we quickly applied somewhere React.useState, somewhere - React.useEffectand continued to do our job. This approach, however, does not allow to take full advantage of all the opportunities that hooks are capable of giving. Presenting React.useStatein the form of a small use<StateName>State, and React.useEffect- in the form use<EffectName>Effect, we were able to achieve the following:

  1. Components are smaller than with regular hooks.
  2. Hooks can be reused in various components.
  3. , React, , . , State<StateName> State. React.useState, .

Retrieving hooks also contributes to the fact that reusable and shared logic is more visible in various parts of the application. Similar or duplicate logic is more difficult to notice if you only use hooks built into the component code. The resulting hooks can be small in size and contain very little code - like useToggleState. On the other hand, larger hooks, like useProductFetcher, can now include more functionality. Both of these helped us simplify the code base by reducing the size of React components.

The following example creates a small React hook designed to control the selection of elements. The benefits of encapsulating this functionality become apparent immediately after you realize how often such logic is used in the application. For example, to select a set of orders from a list.

//  

function Component() {
    const [selected, setSelected] = React.useState()
    const select = React.useCallback(id => setSelected(/** ... */), [
        selected,
        setSelect
    ])

    return <List selectedIds={selected} onSelect={id => select(id)} />
}

//   

const useSelection = () => {
    const [selected, setSelected] = React.useState()
    const select = React.useCallback(id => setSelected(/** ... */), [
        selected,
        setSelect
    ])

    return [selected, select]
}

function Component() {
    const [selected, select] = useSelection()

    return <List selectedIds={selected} onSelect={id => select(id)} />
}

2. About the benefits of React.useDebugValue


The standard React.useDebugValue hook refers to the little-known React features. This hook can help the developer during the debugging of the code; it can be useful for using it in hooks designed for sharing. These are user hooks that are used in many components of the application. However, it is useDebugValuenot recommended to use it in all user hooks, since the built-in hooks already log standard debug values.

Imagine creating a custom hook designed to make a decision about the need to enable some feature of the application. The application state data on which the decision is based can come from different sources. They can be stored in a React context object, access to which is organized through React.useContext.

In order to help the developer in debugging, while working with React developer tools, it may be useful to know about the analyzed flag name ( flagName) and the variant of the flag value ( flagVariation). In this case, using a small hook can help us React.useDebugValue:

export default function useFeatureToggle(flagName, flagVariation = true) {
    const flags = React.useContext(FlagsContext)
    const isFeatureEnabled = getIsFeatureEnabled(flagName, flagVariation)(flags)

    React.useDebugValue({
        flagName,
        flagVariation,
        isEnabled: isFeatureEnabled
    })

    return isFeatureEnabled
}

Now, working with the tools of the React developer, we can see information about the option of the flag value, about the flag name, and whether or not the feature of interest to us is turned on.


Working with React developer tools after applying the React.useDebugValue hook

Note that in cases when user hooks use standard hooks likeReact.useStateorReact.useRef, such hooks already log the corresponding status or ref-object data. As a result, the use of view constructs hereReact.useDebugValue({ state })is not particularly useful.

3. The combination and composition of hooks


When we began to implement hooks in our work and began to use more and more hooks in components, it quickly turned out that 5-10 hooks could be present in the same component. In this case, hooks of various types were used. For example, we could use 2-3 hooks React.useState, then a hook React.useContext(say, to get information about the active user), a hook React.useEffect, and also hooks from other libraries, like react-router or react-intl .

The above was repeated again and again, as a result, even very small components were not so compact. In order to avoid this, we began to extract these individual hooks into user hooks, the device of which depended on the component or some application capability.

Imagine an application mechanism aimed at creating an order. When developing this mechanism, many components were used, as well as hooks of various types. These hooks can be combined as a custom hook, increasing the usability of the components. Here is an example of combining a set of small hooks in one hook.

function OrderCreate() {
    const {
        orderCreater,
        orderFetcher,
        applicationContext,
        goToNextStep,
        pendingOrder,
        updatePendingChanges,
        revertPendingChanges
    } = useOrderCreate()

    return (/** ...children */)
}

4. Comparison of React.useReducer and React.useState


We often resorted to React.useStateas a standard hook for working with the state of components. However, over time, the state of the component may need to be complicated, which depends on the new requirements for the component, like the presence of several values ​​in the state. In certain cases, use React.useReducercan help avoid the need to use multiple values ​​and simplify the logic of updating the state. Imagine state management when making HTTP requests and receiving responses. To do this, you may need to work with values ​​such as isLoading, dataanderror. Instead, the state can be controlled using a reducer, having at its disposal various actions to manage state changes. This approach, ideally, encourages the developer to perceive the state of the interface in the form of a state machine.

The reducer transmitted React.useReduceris similar to the reducers used in Redux , where the system receives the current state of the action and should return the next state. The action contains information about its type, as well as data on the basis of which the next state is formed. Here is an example of a simple reducer designed to control a certain counter:

const initialState = 0;
const reducer = (state, action) => {
    switch (action) {
        case 'increment': return state + 1;
        case 'decrement': return state - 1;
        case 'reset': return 0;
        default: throw new Error('Unexpected action');
    }
};

This reducer can be tested in isolation and then used in the component using React.useReducer:

function Component() {
    const [count, dispatch] = React.useReducer(reducer, initialState);

    return (
        <div>
            {count}
            <button onClick={() => dispatch('increment')}>+1</button>
            <button onClick={() => dispatch('decrement')}>-1</button>
            <button onClick={() => dispatch('reset')}>reset</button>
        </div>
    );
};

Here we can apply what we examined in the previous three sections, namely, we can extract everything into useCounterReducer. This will improve the code by hiding action type information from a component that describes the appearance of an interface element. As a result, this will help prevent leakage of implementation details into the component, and will also give us additional opportunities for debugging code. Here's what the resulting custom hook and the component using it will look like:

const CounterActionTypes = {
    Increment: 'increment',
    Decrement: 'decrement',
    Reset: 'reset',
}

const useCounterReducer = (initialState) => {
    const [count, dispatch] = React.useReducer(reducer, initialState);

    const increment = React.useCallback(() => dispatch(CounterActionTypes.Increment));
    const decrement = React.useCallback(() => dispatch(CounterActionTypes.Decrement));
    const reset = React.useCallback(() => dispatch(CounterActionTypes.Reset));

    return {
        count,
        increment,
        decrement
    }
}

function Component() {
    const {count, increment} = useCounterReducer(0);

    return (
        <div>
            {count}
            <button onClick={increment}>+1</button>
        </div>
    );
};

5. Gradual implementation of hooks


At first glance, the idea of ​​gradually introducing hooks may not seem entirely logical, but here I suggest that those who think so simply follow my reasoning. Over time, various patterns find application in the codebase. In our case, these patterns include higher-order components, rendering properties, and now hooks. When translating a project to a new pattern, developers do not seek to instantly rewrite all the code, which, in general, is almost impossible. As a result, you need to develop a plan for transferring the project to React hooks, which does not include major changes to the code. This task can be very difficult, since making changes to the code usually leads to an increase in its size and complexity. By introducing hooks, we strive to avoid this.

Our codebase uses class-based components and functional components. Regardless of which component was used in a certain situation, we sought to share logic through React hooks. First, we implement the logic in hooks (or repeat the existing implementation of certain mechanisms in them), then create small components of a higher order, inside which these hooks are used. After that, these higher-order components are used to create class-based components. As a result, we have at our disposal logic located in one place, which can be used in components of various kinds. Here is an example of implementing hook functionality in components through higher order components.

export const injectTracking = (propName = 'tracking') => WrappedComponent => {
    const WithTracking = props => {
        const tracking = useTracking();

        const trackingProp = {
            [propName]: tracking,
        };

        return <WrappedComponent {...props} {...trackingProp} />;
    };

    WithTracking.displayName = wrapDisplayName(WrappedComponent, 'withTracking');

    return WithTracking;
};

export default injectTracking;

This shows the implementation of hook functionality useTrackingin a component WrappedComponent. This, by the way, allowed us, among other things, to separate the tasks of implementing hooks and rewriting tests in the old parts of the system. With this approach, we still have at our disposal mechanisms for using hooks in all parts of the code base.

Summary


Here were some examples of how using React hooks improved our codebase. We hope that hooks can benefit your project as well.

Dear readers! Do you use React hooks?


All Articles