Why use Reducer hooks for state management in React?
useState() vs useReducer()
useState()
useState() manages a single state variable, it is the most atomic state management hook in React.
const [counter, setCounter] = useState(0);
It returns the state variable that you can bind your components or logic to. It also provides a setter function that you can update the state’s value with. Modifying the state variable directly has no effect on the data binding. useState can also manage arrays and objects. Even nested ones. One thing you have to make sure while managing arrays and objects via useState is to enforce immutability. Which means that your setXXX function should always use a fresh or cloned object. With ES6, spread operator comes in quite handy. For deeply nested objects, use lodash or ramda to create a deep clone.
Pros
- Simple
Cons
- Your code can be littered with setter calls making it very tough to track state mutations
- setter is asynchronous. Accessing state right after calling a setter will not give you the new state. You can use it within a setTimeout cycle to overcome this.
What is a reducer?
When you hear the word reducer, what comes to mind is a large function with a switch statement at the heart of it. This is probably how many of us are used to it; in the context of Redux. However, a reducer is just a pure function; a function that takes two arguments, combines them in some meaningful way and returns a new result.
In the context of state management, a reducer takes the current state as the first argument and an action object as the second. It returns a new state based on these two pieces of data.
Side Note: The Redux recommendation of using switch based reducers is imperative but it reads well and is very familiar. I have not found a compelling reason to apply Open/Closed principle and make it an unfamiliar pattern yet.
useReducer()
useReducer is a React hook that helps us manage a state object via reducers.
const [state, dispatch] = useReducer(reducerFunction, initialState);
It returns:
- a state object that you can bind your components to.
- a dispatch function that you can send actions to.
You can in fact write your own useReducer hook like so:
const useReducer = (reducer, initialState) => {
const [state, setState] = React.useState(initialState); const dispatch = (action) => {
setState(reducer(state, action));
} return [state, dispatch];
}
Pros
- Manage all your state mutations in one place.
- Reducers are pure functions and very unit testable
- Reducers are handy for creating small pools of isolated data contexts. You can use it in conjunction with React Context API to keep your state where it belongs.
Cons
- Boiler plate code that results from having to define actions, action creators and the switch based reducer.
- Long functions resulting from the switch statements
- A bit of a learning curve for new devs.
Which one should you chose?
It is fairly straightforward to decide. Use useState if:
- The states are simple types such as boolean, string etc, or shallow objects
- There are only a handful you are managing
- State setters are not littered all over the place in your component.
Otherwise use useReducer.