React is a library for creating front end views. It has a big ecosystem of libraries that work with it. Also, we can use it to enhance existing apps.
In this article, we’ll look at the useContext
and useReducer
hooks.
useContext
We can use the useContext
hook to read shared data shared from a React context. It accepts the context object returned from React.createContext
as an argument and returns the current context value.
The current context value is determined by the value
prop of the nearest context provider.
We can use it as follows:
const ColorContext = React.createContext("green");function Button() {
const color = React.useContext(ColorContext);
return <button style={{ color }}>button</button>;
}function App() {
return (
<>
<ColorContext.Provider value="blue">
<Button />
</ColorContext.Provider>
</>
);
}
In the code above, we created a new React context with:
const ColorContext = React.createContext("green");
Then in App
, we wrapped out Button
with the ColorContext.Provider
with the value
prop set to blue
.
Then in Button
, we have:
const color = React.useContext(ColorContext);
to get the value passed in from the ColorContext.Provider
and set it to color
.
Finally, we set the color
style of the button
with the color
‘s value.
A component calling useContext
will always re-render when the context value changes. If re-rendering is expensive, then we can optimize it with memoization.
useContext
is the React hooks version of Context.Consumer
.
useReducer
This hook is an alternative to useState
. It accepts a reducer function of type (state, action) => newState
.
useReducer
is preferable to useState
when we have complex state logic that involves multiple sub-values or when the next state depends on the previous one.
It also lets us optimize performance for components that trigger deep updates because we can pass dispatch
down instead of callbacks.
For example, we can write:
const INCREMENT = "INCREMENT";
const DECREMENT = "DECREMENT";function reducer(state, action) {
switch (action.type) {
case INCREMENT:
return { count: state.count + 1 };
case DECREMENT:
return { count: state.count - 1 };
default:
throw new Error();
}
}
function App() {
const [state, dispatch] = React.useReducer(reducer, { count: 0 });
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: DECREMENT })}>Decrement</button>
<button onClick={() => dispatch({ type: INCREMENT })}>Increment</button>
</>
);
}
In the code above, we have our reducer
which returns the new state depends on the action.type
‘s value. In this case, it’s either 'INCREMENT'
or 'DECREMENT'
.
If it’s ‘INCREMENT’
, we return { count: state.count + 1 }
.
If it’s ‘DECREMENT’
, we return { count: state.count — 1 }
.
Otherwise, we throw an error.
Then in App
, we call useReducer
by passing in a reducer
as the first argument and the initial state as the second argument.
Then we get the state
object, which has the current state object and a dispatch
function, which we can call with an action
object, which has the type
property with the value being one of ‘INCREMENT’
or ‘DECREMENT'
.
We used the dispatch
function in the buttons to update the state.
Finally, we display the latest state in state.count
.
Lazy initialization
We can pass in a function to the 3rd argument of useReducer
to initialize the state lazily.
The initial state will be set to init(initialArg)
.
For instance, we can rewrite the previous example as follows:
const init = initialCount => {
return { count: initialCount };
};
const INCREMENT = "INCREMENT";
const DECREMENT = "DECREMENT";
function reducer(state, action) {
switch (action.type) {
case INCREMENT:
return { count: state.count + 1 };
case DECREMENT:
return { count: state.count - 1 };
default:
throw new Error();
}
}
function App() {
const [state, dispatch] = React.useReducer(reducer, 0, init);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: DECREMENT })}>Decrement</button>
<button onClick={() => dispatch({ type: INCREMENT })}>Increment</button>
</>
);
}
First, we have:
const init = initialCount => {
return { count: initialCount };
};
to return the initial state.
And instead of writing:
React.useReducer(reducer, { count: 0 });
We have:
React.useReducer(reducer, 0, init);
0 is passed in as the initialCount
of init
.
Then the rest of the code is the same as before.
Bailing out of a dispatch
If the same value is returned from a Reducer hook is the same as the current state, React will bail out without rendering the children or firing effects.
The comparison is done using the Object.is()
algorithm.
If we’re doing expensive operations while rendering, we can optimize it with useMemo
.
Conclusion
The useContext
hook is the React hook equivalent of the Context.Consumer
of the Context API.
It takes a React context object as the argument and returns the current value from the context.
useReducer
is an alternative version of useState
for more complex state changes.
It takes in a reducer as the first argument and the initial state object as the second argument.
It can also take the same first argument, and take the initial state value as the second argument, and a function to return the initial state as the 3rd argument. This combination lets React set the initial state lazily.