To be honest useReducer hook feels like unga bunga hook if you are using it for the first time, but this is simple uncluttered article to get familiar with useReducer hook. If you have used useState
hook for managing the state like simple strings, arrays, objects you are going to have good time here. useReducer
is used for this exact purpose that is for managing state. but it gives more flexibility for us to control and update the state.
As a matter of fact useState is built on top of useReducer
you can think of useState
hook as a simplified version of useReducer
**
let's see how to use useReducer
hook first. then we will deep dive into this hook
const countReducer = (prevState, action) => {
console.log(action)
// logs whatever we passed in dispatch(/* smth */)
// Ex: if we dispatch('bob') in <Counter />, output: 'bob'
// if we do dispatch(1) in <Counter />, output: 1
return action // this value is our `newState` that reflects on `count` we get from useReducer
}
function Counter({initialCount: 0}) {
const [count, dispatch] = useReducer(countReducer, initialCount)
return <button onClick={() => dispatch(count + 1)}>{count}</button>
}
Here we just made a counter which we can increment on a button click.
- We will get
[state, dispatch]
array fromuseReducer
useReducer
takes Reducer function, initial state as arguments- Reducer function accepts two arguments
reducerFn(previousState, action)
and returnsnewState
You have to focus on terms state, dispatch, action, reducerFunction here
- state: count is the state we want to manage here
- dispatch: dispatch is a function through which we set the 'action value' that we accept through our reducer function.
- action: action is a value whatever we passed through function.
- reducerFunction: it is a function through which we take control over and manage our state.
Flow example:
- Let's say we clicked the button which calls dispatch(count + 1)
- This dispatch value go through countReducer function where the new state will be returned.
- Now
action
value is equal tocount + 1
which is 0 + 1. (action
= 1) - We are returning the
action
value in the reducerFunction - So our
<Counter />
will rerender because of our state is updated. - Now useReducer gives
[count, dispatch]
array where, our count value has updated to1
let's add some more things to our counter example with useReducer
function countReducer(prevState, action) {
const {type, step} = action
switch (type) {
case 'increment': {
return {
count: prevState.count + step,
}}
case 'decrement': {
return {
count: prevState.count - step,
}}
default: {
throw new Error(`Unsupported action type: ${type}`)
}}
}
function Counter({initialCount = 0, step = 1}) {
const [state, dispatch] = React.useReducer(countReducer, {
count: initialCount,
})
const {count} = state
const increment = () => dispatch({type: 'increment', step})
const decrement = () => dispatch({type: 'decrement', step})
return {
<>
<h1>{count}</h1>
<button onClick={increment}>increment</button>
<button onClick={decrement}>decrement</button>
</>
}
}
Here is the codesandbox
In this example, we are taking advantage of how dispatch
and action
work unlike the previous example.
whatever we give to the dispatch, the same will be reflected on action
then we used switch case. if action
is a particular value to do a particular operation. this is how most people use action, to navigate and tell what the reducer function should do to give us a new state.
now, I'm sure you got a good understanding of how useReducer works. Let's see some more examples and scenarios to learn more about this hook.
Let's add an input field to our previous example..
ps: the following code has a bug! Can you spot where it is?
import "./styles.css";
import React from "react";
function countReducer(prevState, action) {
const { type, step } = action;
console.log(prevState);
switch (type) {
case "increment": {
return {
count: prevState.count + step,
};
}
case "decrement": {
return {
count: prevState.count - step,
};
}
case "usernameChange": {
return {
username: action.username,
};
}
default: {
throw new Error(`Unsupported action type: ${type}`);
}
}
}
export default function Counter({ initialCount = 0, step = 1 }) {
const [state, dispatch] = React.useReducer(countReducer, {
count: initialCount,
username: "",
});
const { count, username } = state;
const increment = () => dispatch({ type: "increment", step });
const decrement = () => dispatch({ type: "decrement", step });
const handleChange = (event) => {
dispatch({ type: "usernameChange", username: event.target.value });
};
return (
<div className="App">
<h1>{count}</h1>
<button onClick={increment}>increment</button>
<button onClick={decrement}>decrement</button>
<div>
<label>
Username:
<input type={"text"} value={username} onChange={handleChange} />
</label>
</div>
<h1> {username} </h1>
</div>
);
}
here we have added username
to our state. now we can manage our state seamlessly
but hang on.. we have a bug in the above code.
I did it intentionally to reach it in your core memory because it is a very common mistake people do.
the bug here is, whenever you change a state, for example take increment
case
case "increment": {
return {
count: prevState.count + step
}}
we are esentially overriding the whole state with just count in the object. the new state will set to {cont: prevState.count + step}
we loose our username
state.
This is why we should spread the previous state in the object we are returning.
this is the correct way of doing it
case "increment": {
return {
+ ...prevState,
count: prevState.count + step
}}
We should always spread the previous state value in the return object. else we'll end up overriding bunch of other states.
Here is the codesandbox for this example
tip : If you are managing similar state with the useState
hook you can do like this
const [countState, setCountState] = React.useState({ count: 0, name: "" });
const handleClick = () =>
setCountState((prevState) => {
return { ...prevState, count: prevState.count + 1 };
});
oh wait a min.. if we can do this whole state thing simply using useState hook what's the point of using this complicated stuff?!! yeah right, you don't need to use this if useState can get the job done. most people make use of useReducer in codebases for the sake of maintainability, readability and to have better control over the state espicially in Forms.
When to use useReducer:
it's wise to use useReducer when one element of your state relies on the value of another element of your state in order to update or when the state consists of more than primitive values, like nested object or arrays. if not, you are better off using useState
Tips:
- Do not mix up and add lots of unrelated states into a single reducer.
- whenever a single state changes you rerender the components which are using this state managed by reducer to get the whole state and UI updated.
- Imagine one of the component that is so heavy to render and takes lots of resources and time and you re render it unnecessarly, the perfomance might degrade.
This is why you should only put states which are interrelated together in a single reducer and make separate reducers in order to avoid unnecessary rerenders.