I don’t recommend using Context/useReducer anymore. With libraries like Redux Toolkit, Zustand and others, you'll get much better performance without having to re-invent the wheel. If you’re working on a trivial project, by all means do it. If you expect it to grow, do yourself the favour and use something that will take you far.
'Props drilling' is the process of passing down data to your components level after level.
You start simple with two child components. Then you have to add more features and inevitably the two original components have children of their own. You start moving your callbacks further and further down the component hierarchy, but ultimately the code works.
It doesn’t feel nice though since we have components that don’t care about most of their props, they just act as middlemen. To add more insult to injury:
- Refactoring is painful
- Needs more work to avoid unnecessary re-rendings
- There is a lot of code smell
Here’s an example. We have an ItemsList
component that semantically groups everything related to the list of our items.
Now the ItemsTable
can be broken down in more components. And the ItemRow
could be broken down in even more if the table cell contents are editable.
In medium to large applications, with an overload of information people resort to state management solutions like Redux or MobX.
While we can use one of either, in our use case we don’t need a state-management library, as we are only about to use a small subset of their features.
Thankfully the React team in 16.3.0 updated their context API that these libraries rely on.
What is a game-changer for me now, it’s the combination with the newly released React Hooks. It offers a greatly streamlined development experience, let’s dive in!
Note: You can also Composition. Here’s a tweet demonstrating this approach https://twitter.com/mjackson/status/1195495535483817984 - To be frank, I believe it’s easy to get out of hand, but works wonders for few levels of nesting.
Context + Hooks
Alright, in this case, I’ll omit using the usual state pattern and take a page out of Redux’s book. Hooks introduced the useReducer hook, and that works wonders since we can use the same dispatch
callback all over our codebase.
The “store” declaration is in the same file purely for demonstration purposes
Let’s try to redo the previous snippet.
Now these ComponentX
components, can be totally agnostic of the data that their children need.
Let’s move on to our nested ItemRow
component. We only care about the dispatch handler for now so let’s just introduce this handleDelete
. In this naive example, the moment we trigger the button, the component will be unmounted so we don’t care about extra memoization in the handleDelete
callback.
The reason why DELETE_ITEMS
expects an array of ids, is so that it can be reused when we have multi-row controls.
Now if we wanted to add more functionality in the above component, that wouldn’t be that hard!
Let’s add the row selection functionality. Previously, we could have to include the handleSelection
callback in every component between App
& ItemRow
. Now, we'll stick to using dispatch
with a different type.
Since we use hash-map instead of an array in our state, we can add this to our reducer
Adding the extra table column, as well as the onClick
handler and we're good!
In case you wondered why we include dispatch
in the useCallback
dependencies, that's straight from the docs:
React guarantees that dispatch function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list.
Fin
That is all.
React hooks are a breath of fresh air and I really enjoy utilizing them more and more. That being said, as we move on to functions rather than classes, it’s good to be extra precautious about caching all your expensive computations and callbacks.
The official documentation is probably the best resource out there, so that's all you need. I would strongly recommend using the accompanying eslint plugin, which helps immensely.
As for the Context API, let’s keep in mind that it’s a simple way to tunnel data to components further down the component tree. It’s not a replacement for Redux, even if they tend to accomplish the same goals in small scale projects.
Redux might be using Context internally, but also enables great features like the DevTools plugin, 'time-travel' and more. I would point out you to You Might Not Need Redux by Dan Abramov, because who better to reason about it, that him?