Passing callbacks down with React Hooks

Ceci García García
Trabe
Published in
4 min readApr 8, 2019
Photo by zhenhao Liu on Unsplash

A common problem in React involves passing props from a component down through several layers of components.

Below is an example of a component that consists in a list of tasks and some actions to manage that tasks. The <TodoList /> keeps the state of the list, so the callbacks to manage it should be passed down to the components that will use them:

Instead of having to pass each callback down the tree in every component, we can define an API object that collects all callbacks. This way, in case we need to add/remove some callbacks, we won’t have to adapt every component in the hierarchy.

We could simplify the example above even more. To avoid the hell of passing down through every component in the hierarchy, we could pass the “API object” via React Context with the useContext Hook.

The problem is that the API object will change in every rerender, so all components that read it from the context will be rerendered as well.

The pattern recommended by the React team

We talked a little about this problem in a previous post and pointed to the pattern recommended by the React team to solve it. The pattern is a refactor of the example above but using useReducer instead of useState. Managing the state with useReducer allows us to pass just the dispatcher down and, since the dispatcher doesn’t change between renders, the components that read it from the context won’t be rerendered:

Our thoughts about the recommended pattern

This solution definitely solves our problem, but still we didn’t feel fully satisfied with it because:

  • We have to turn “dumb components” (only concerned with how things are painted) into “smart components” (also concerned with how things work): In the example above, the action components <AddTodoBtn />, <RemoveAllBtn /> can be dumb components if they only have to get the onClick handler from the context instead of getting the dispatcher and deciding which action they have to dispatch.
  • We’re forced to use the useReducer Hook to manage the state. It doesn’t matter in simple examples as the one above, but in more complex scenarios it could restrict us from using custom Hooks already implemented using useState to manage the state.

How we solve this problem

Our solution lies in combining the two previous examples:

  • Using the context to pass variables down to the components that will use them.
  • Using an “API object” that contains all the callbacks that the nested components will need (this callbacks can use either useState or useReducer to manage the state).
  • Memoizing the creation of the “API object” to make sure it won’t change through rerenders.
  • Making sure all callbacks defined in the API object don’t depend on their scope.

We’ve already see how to pass an API object using the React useContext Hook in the first example so let’s see how we can memoize this API object.

Memoizing an API object

To memoize an API object we have to memoize both the API object itself and its callbacks. We use the useMemo Hook to memoize the API object and the useCallback Hook to memoize the callbacks:

Ok, we’ve memoized the API object so it won’t change between rerenders, but the action depending on the scope (addTodo depends on the todo state value) won’t be consistent over rerenders since the value of the state variable won’t change.

One possible solution could be to add the todos state variable to the array of dependencies (second argument) of the useCallback Hook and the useMemo Hook. This way, both callback and API object will update every time the state variable changes:

If the memoization of the API object depends on the scope where it’s defined, we just have partial memoization.

If we want our API object to never change between rerenders, we need our callbacks to be independent of the scope. We can do this by updating the state with functional updates: We can pass a function to setState, the function will receive the previous value of the state, and return the updated value.

Our API object is now completely independent from the scope so, as the dispatch function, it won’t change between rerenders! Yay!

To sum up

To avoid passing callbacks down through the component hierarchy, we can follow the suggested pattern of passing the dispatch function of the useReducer using the Context, but we end up being forced to manage the state via a reducer, and we have to spread some knowledge about the state through the components that use this callbacks.

We suggest to do this by passing a memoized API object using the context. To achieve the complete invariability of the API object, we use functional updates so we don’t depend on the scope of the callbacks.

--

--