Five practical tips when using React hooks in production

Tobias Deekens
commercetools tech
Published in
6 min readMar 9, 2020

--

Since React hooks were introduced in early 2019 with React 16.8.0, we at commercetools were early adopters and have since been continuously refactoring our code base to make use of them. React hooks allow the use of state and other React features without writing a JavaScript class. By using them you can “hook into” the underlying lifecycle and state changes of a component within a functional component.

Too long; didn’t read

React hooks helped us to simplify our code base by extracting logic out of components and composing different functionality more easily. Furthermore, adopting React hooks resulted in many lessons for us, such as how to structure our code better through the use of constant iterations on existing functionality. We are sure we will learn more tricks and improved ways to use React hooks, but we’re curious what yours are! Find us on Twitter and GitHub.

Benefits we’ve gained from adopting React hooks

  • Readability: via a smaller component tree by avoiding render props and Higher-Order Components (HoCs)
  • Debuggability: using improved visual representation and additional debug values provided in React Dev Tools
  • Modularity: an easier way to compose and build reusable logic due to the functional nature of hooks
  • Separation of concerns: React components focus on the visual representation while hooks expose encapsulated business logic

1. Extract and build custom hooks early

It is quite easy to start using React hooks in your Functional Components. We quickly dropped in a React.useState here and a React.useEffect there and moved on. This however does not fully utilize all advantages we could see from adopting hooks. By extracting a React.useState into a small use<StateName>State and a React.useEffect into a use<EffectName>Effect, we were able to:

  1. Keep our React components even leaner and shorter
  2. Allow reuse of the same hook across multiple components
  3. Provide more descriptive naming within React Developer Tools (for instance, State<StateName> over just State which becomes unorganized with multiple usages of React.useState within a single component

Extracting hooks also makes reusable and shared logic more visible across different parts of the application. Similar or duplicated logic is harder to spot when only inlining hooks. The resulting React hooks can be small and contain little logic such as a useToggleState. On the other hand, bigger hooks like a useProductFetcher are now able to contain more functionality. Both cases helped us simplify our code base by keeping React components leaner.

The example below illustrates building a small React hook to manage selection state. The advantages in encapsulating this functionality quickly become apparent when you realize how often a selection logic occurs inside of an application, for example to select a set of orders from a list.

A custom hooks to manage selection state

2. Sometimes React.useDebugValue helps

The built-in React.useDebugValue is a lesser known hook which can help with debugging and can be useful in shared hooks. Shared hooks are custom defined hooks which are used by multiple components in your application. However, it is not recommended for use in every custom hook as built-in hooks already log default debug values.

Imagine building a custom React hook to evaluate if a feature should be enabled or not. The state backing this system could come from various sources and would be stored on a React context accessible through React.useContext.

To help debugging, it would be helpful to know what feature flag name and variation was evaluated in the React Developer Tools. A small React.useDebugValue can help here:

Adding a React.useDebugValue to provide for context in the React Developer Tools

When now looking at the React Developer Tools we would see the following info about flag variation and flag name, and the resulting status of the feature:

The result in the React Developer Tools

Note that whenever a built-in hook, such as React.useState or React.useRef, is used in a custom hook, it will already debug its respective state or ref value within the React Developer Tools. As a result React.useDebugValue({ state }, is not incredibly useful.

3. Combine and compose hooks

When we started adopting and using more React hooks, we quickly ended up using about 5–10 hooks within a single component. The type of hooks we use varies a lot. We might use 2–3 React.useState hooks, then a React.useContext (for instance, to get information for the currently active user), a React.useEffect and hooks by other libraries such as react-router or react-intl.

The pattern above repeated and small-ish React components turned out to be not so small after all. To avoid this we started to extract these individual hooks into custom hooks, depending on the component or feature. Imagine building an order creation feature. This feature is built using multiple components as well as hooks of different types. These can be combined into custom hooks, making their consumption more convenient.

Combining a set of small hooks into a single one

4. React.useReducer vs. React.useState

We often resorted to React.useState as the default hook to keep the state in our components. However, over time the component state might need to get more complex, depending on the new requirements like having multiple state values. In certain cases, using a React.useReducer hook can help to avoid multiple state values and simplifies the state update logic. Imagine managing a HTTP request/response state. This could require multiple state values for isLoading, data, and error. You can instead have the state managed by a reducer and have specific actions to update the state permutations. This ideally also guides thinking of the states within your interface as a state machine.

A reducer passed to React.useReducer is similar to a reducer in Redux, where it receives the current state of an action and is meant to return the next state. The action contains the type and payload to derive the next state. In a contrived counter example a reducer could look like this:

An isolated reducer to manage counter state

This reducer can be tested in isolation and then used within a React.useReducer with the following function:

Using the reducer in a React component

We can further apply what we learned in the previous three sections by extracting everything into a useCounterReducer. This improves our code by hiding the action types from the view. As a result preventing lacking implementation details into the views while also enabling additional debugging.

Extracting into a custom hook

5. Adopt hooks gradually

This may seem a bit counter-intuitive at first but bear with me. Over time code bases adopt different patterns. In our example, these patterns include HoCs (Higher-order Components), render props and now hooks. When migrating code it is not desired and unfeasible to change everything at once. As a result we needed a migration path towards React hooks without requiring large rewrites. This can be challenging as changes naturally tend to grow size and concerns — and that’s something we try to avoid with our adoption of hooks.

Our codebase uses Class Components and Functional Components. No matter which type of component was used, we wanted to share logic through our React hooks. We first (re)implemented the logic in hooks, before we exposed small HoCs that internally use the hooks and expose their functionality Class Components. As a result we ultimately have the logic in one place that can be used by different kinds of components again.

Injecting functionality of a hook through a HoC

The example above injects the functionality of a useTracking hook into the wrapped component. This incidentally allowed us to also split adopting hooks and rewriting our tests in older parts of the system. Still offering an early migration path towards hooks in all parts of our code base.

Closing words

Those are some ways React hooks helped with our quality of life, and can hopefully do the same for you in your development workflow. If you have any tips or questions, reach out to us on Twitter or GitHub!

--

--