-
Book Overview & Buying
-
Table Of Contents
-
Feedback & Rating
Micro State Management with React Hooks
By :
In this section, we will learn how to use useReducer. We will learn about its typical usage, how to bail out, its usage with primitive values, and lazy initialization.
A reducer is helpful for complex states. Here's a simple example a with two-property object:
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'SET_TEXT':
return { ...state, text: action.text };
default:
throw new Error('unknown action type');
}
};
const Component = () => {
const [state, dispatch] = useReducer(
reducer,
{ count: 0, text: 'hi' },
);
return (
<div>
{state.count}
<button
onClick={() => dispatch({ type: 'INCREMENT' })}
>
Increment count
</button>
<input
value={state.text}
onChange={(e) =>
dispatch({ type: 'SET_TEXT', text: e.target.value })}
/>
</div>
);
};
useReducer allows us to define a reducer function in advance by taking the defined reducer function and initial state in parameters. The benefit of defining a reducer function outside the hook is being able to separate code and testability. Because the reducer function is a pure function, it's easier to test its behavior.
As well as useState, bailout works with useReducer too. Using the previous example, let's modify the reducer so that it will bail out if action.text is empty, as follows:
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'SET_TEXT':
if (!action.text) {
// bail out
return state
}
return { ...state, text: action.text };
default:
throw new Error('unknown action type');
}
};
Notice that returning state itself is important. If you return { ...state, text: action.text || state.text } instead, it won't bail out because it's creating a new object.
useReducer works for non-object values, which are primitive values such as numbers and strings. useReducer with primitive values is still useful as we can define complex reducer logic outside it.
Here is a reducer example with a single number:
const reducer = (count, delta) => {
if (delta < 0) {
throw new Error('delta cannot be negative');
}
if (delta > 10) {
// too big, just ignore
return count
}
if (count < 100) {
// add bonus
return count + delta + 10
}
return count + delta
}
Notice that the action (= delta) doesn't have to have an object either. In this reducer example, the state value is a number—a primitive value—but the logic is a little more complex, with more conditions than just adding numbers.
useReducer requires two parameters. The first is a reducer function and the second is an initial state. useReducer accepts an optional third parameter, which is called init, for lazy initialization.
For example, useReducer can be used like this:
const init = (count) => ({ count, text: 'hi' });
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'SET_TEXT':
return { ...state, text: action.text };
default:
throw new Error('unknown action type');
}
};
const Component = () => {
const [state, dispatch] = useReducer(reducer, 0, init);
return (
<div>
{state.count}
<button
onClick={() => dispatch({ type: 'INCREMENT' })}
>
Increment count
</button>
<input
value={state.text}
onChange={(e) => dispatch({
type: 'SET_TEXT',
text: e.target.value,
})}
/>
</div>
);
};
The init function is invoked just once on mount, so it can include heavy computation. Unlike useState, the init function takes a second argument—initialArg—in useReducer, which is 0 in the previous example.
Now we have looked at useState and useReducer separately, it's time to compare them.