Five practical tips when using React hooks in production
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:
- Keep our React components even leaner and shorter
- Allow reuse of the same hook across multiple components
- Provide more descriptive naming within React Developer Tools (for instance,
State<StateName>
over justState
which becomes unorganized with multiple usages ofReact.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.
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:
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:
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.
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:
This reducer can be tested in isolation and then used within a React.useReducer
with the following function:
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.
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.
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!