Step-by-Step evolution of using useContext and useReducer in React/React-Native
Many articles exist online about useContext
and useReducer
in React/React-Native. So why do we need yet another one on the same topic? The justification is that I want to show a step-by-step evolution of how useContext
and useReducer
help simplify code and separate state management from UI logic. By the end of this article, the benefit of combining these two hooks would be self-evident.
Sample Task
Suppose we want to implement “use dark mode” feature on a web app, where we have a global checkbox that can set dark mode for three panels. In addition, we want each panel to have its own “use dark mode” checkbox to behave the same as the global checkbox, i.e., when the “use dark mode” checkbox of one panel is checked, all the other panels turn to dark mode as well (this example is adapted from React’s documentation).
Implementation with useState
The most straightforward solution is to use useState
, as shown below. The main drawback is that it is not scalable. What if we have many panels, or even panel within panel? We cannot copy and paste theme={theme} setTheme={setTheme}
all the time. This problem is also known as props drilling.
Implementation with useState + useContext
useContext
resolves the scalability problem. It has a simple concept: provide one state such that all components under the state can access its value without having to pass the props around.
Some syntax change is needed (shown below), but it’s not too bad. The benefit is significant, as we no longer have to pass the state and state management function to Panel
via props.
However, there is still an issue. We define setTheme
, i.e., the logic of how theme
is updated, and theme
itself in the UI code. What if we accidentally change the default value of theme
or the logic of setTheme
? How much cleaner and easier to manage our code it would be if we separate the logic of theme
and setTheme
from UI code! We will surely appreciate this decoupling if our state gets enormous and complicated.
Implementation with useReducer + useContext
To separate state and state management from UI code, we can switch useState
with useReducer
. useReducer
has a straightforward concept: it is essentially the same as useState
, except that it makes state update independent of any component (in contrast, useState always has to be tied to a component).
As usual, some syntax change is needed (as shown below). But again, it is not too bad. The benefit is tremendous because the state update logic is completely hidden from the UI. For instance, when UI wants to set the theme to dark, all it does is call setDark
, which just says to dispatch an object that contains “SET_DARK”
as its type. What does “SET_DARK”
actually do in terms of state updates? UI does not know, nor should it care.
With the combination of useReducer
and useContext
, we completely separate the state and state management from UI. We can take one more step to restructure our app such that the decoupled code live in separate modules.
New App Structure
The new app structure adds a context
folder. In this folder, we have an actions
module defining what actions are allowed for state update; a reducer
module that holds the state update logic; a store
module that sets up the initial state, context, and provider.
These three modules are considered boilerplate code, but further modification is easy once they are set up. However, the major benefit is that our App
module now looks pristine. It does not know anything about state management; all it does is use theme
, or call the two black-box functions to update theme
. The decoupling of state management and UI code is now complete.
Where does the provider go? It goes to index.js
, wrapping around our entire app so that we can access the context anywhere in the app.
Isn’t this Redux?
Ya, it is Redux-light. The new app structure borrows from Redux, which is clean and easy to manage. Redux itself is a whole other learning curve. I will not pretend I understand it fully, and I recommend that anyone wishing to learn more go for the myriads of articles comparing the workflow of useContext
+ useReducer
with Redux. I will say, however, that for an app with a simple state, useContext
+ useReducer
is sufficient. How can we tell a state is simple? If console.log
is sufficient for all your debugging needs, your state is pretty simple.
Conclusion
I hope this step-by-step, evolution approach sheds some light on the usefulness of useContext
and useReducer
. They both have simple concept, yet once properly combined, they bring out power similar to that of Redux. They are certainly not a panacea, but they are good enough for many regular use cases.