UseReducer — A Side Note to the Official Doc
The following can be read as a side reference to the official React Doc
UseReducer
What is a reducer? For people like me who haven’t get introduced to Reduct, think that the reducer is a pure function that takes the previous state and an action, and returns the next state.
(previousState, action) => nextState
So why do we need a Reducer
In a more complex app, you’re going to want different entities to reference each other. We suggest that you keep your state as normalized as possible, without any nesting. Keep every entity in an object stored with an ID as a key, and use IDs to reference it from other entities, or lists. Think of the app’s state as a database. For example, keeping todosById: { id -> todo }
and todos: array<id>
inside the state would be a better idea in a real app.
A reducer helps you to manage these in-app states. It’s called a reducer because it’s the type of function you would pass to Array.prototype.reduce(reducer, ?initialValue)
.
A pure reducer
It's very important that the reducer stays pure. Things you should never do inside a reducer:
- Mutate its arguments;
- Perform side effects like API calls and routing transitions;
- Call non-pure functions, e.g.
Date.now()
orMath.random()
.
Given the same arguments, it should calculate the next state and return it. No surprises. No side effects. No API calls. No mutations. Just a calculation
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
})
default:
return state
}
}
Note that:
We don’t mutate the state
. We create a copy with Object.assign()
. Object.assign(state, { visibilityFilter: action.filter })
is also wrong: it will mutate the first argument. You must supply an empty object as the first parameter. You can also enable the object spread operator proposal to write { ...state, ...newState }
instead.
We return the previous state
in the default
case. It's important to return the previous state
for any unknown action.
Bailing out of a dispatch
If you return the same value from a Reducer Hook as the current state, React will bail out without rendering the children or firing effects.
Note that React may still need to render that specific component again before bailing out. That shouldn’t be a concern because React won’t unnecessarily go “deeper” into the tree.
How about a payload?
We have actions in the reducer that comes with a type, but what about payload? It’s basically a key in your action which stores the data you want to pass to the reducers.
To separate this type from regular data the payload
property is used. Now, on what should go into payload
and what should be on the same level with it is debatable, but a popular standard is the Flux Standard Action which states that among the official requirements you may add a payload
, error
, and meta
property. Here the payload is defined as:
The optional payload
property MAY be any type of value. It represents the payload of the action. Any information about the action that is not the type
or status of the action should be part of the payload
field. By convention, if error
is true
, the payload SHOULD be an error object.
So an example use case?
Here’s a step-by-step break down of useReducer
hook in React:
- Set initial state
There are two different ways to initialize useReducer
state. You may choose either one depending on the use case. The simplest way is to pass the initial state as a second argument:
const [state, dispatch] = useReducer(
reducer,
{count: initialCount} );
- Create your reducer function (can have logic going in as long as stays pure)
const reducer = (state, action) => {
if (action.type === GRUDGE_ADD) {
return [action.payload, …state];
}
if (action.type === GRUDSGE_FORGIVE) {
return state.map(grudge => {
if (grudge.id !== action.payload.id) return grudge;
return { …grudge, forgiven: !grudge.forgiven };
});
}
return state;
};
- Patch your reducer with your state
const [grudges, dispatch] = useReducer(reducer, initialState);
//grudges is your state
- Dispatch your action when you need to change state (optional payload can be passed in), and return new state
const addGrudge = useCallback(({ person, reason }) => {
dispatch({
type: GRUDGE_ADD,
payload: {
person, reason, forgiven: false, id: id()
}
});
}, [dispatch]);const toggleForgiveness = useCallback(id => {
dispatch({
type: GRUDSGE_FORGIVE,
payload: {id}});
}, [dispatch]);