Passing callbacks down with React Hooks
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 theonClick
handler from the context instead of getting thedispatcher
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 usinguseState
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
oruseReducer
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.