It is common to see useState hook used for state management, However React also have another hook to manage component's state, Which is useReducer hook. In fact, useState is built on useReducer!. So a question arises: What's the difference between the two ? And when should you use either ?
useState hook:
useState hook is a hook used to manipulate and update a functional component. The hook takes one argument which is the initial value of a state and returns a state variable and a function to update it.
const [state, setState] = useState(initialValue)
So a counter app using the useState hook will look like this:
function Counter() {
const initialCount = 0
const [count, setCount] = useState(initialCount);
return (
<>
Count: {count}
<button onClick={() => setCount(initialCount)}>Reset</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
<button onClick={() => setCount(count + 1)}>Increment</button>
</>
);
}
useReducer hook:
this hook is similar to the useState
hook. However it's able to handle more complex logic regarding the state updates. It takes two arguments: a reducer function and an initial state. The hook then returns the current state of the component and a dispatch function
const [state, dispatch] = useReducer(reducer, initialState)
the dispatch
function is a function that pass an action
to the reducer
function.
The reducer
function generally looks like this:
const reducer = (state, action) => {
switch(action.type) {
case "CASE1":
return "new state";
case "CASE2":
return "new state";
default:
return state
}
}
The action is usually an object that looks like this:
// action object:
{type: "CASE1", payload: data}
The type
property tells the reducer what type of action has happened ( for example: user click on 'Increment' button). The reducer
function then will determine how to update the state
based on the action
.
So a counter app using the useReducer hook will look like this:
const initialCount = 0
const reducer = (state, action) => {
switch (action.type) {
case "increment":
return action.payload;
case "decrement":
return action.payload;
case "reset":
return action.payload;
default:
return state;
}
}
function Counter() {
const [count, dispatch] = useReducer(reducer, initialCount)
return (
<>
Count: {count}
<button onClick={() => dispatch({type: "reset", payload: initialCount}))}>Reset</button>
<button onClick={() => dispatch({type: "decrement", payload: state - 1})}>Decrement</button>
<button onClick={() => dispatch({type: "increment", payload: state + 1})}>Increment</button>
</>
);
}
When should i useReducer() ?
As stated above, The useReducer hook handles more complex logic regarding the state updates. So if you're state is a single boolean
, number
, or string
, Then it's obvious to use useState hook. However if your state is an object (example: person's information) or an array (example: array of products ) useReducer will be more appropriate to use.
Let's take an example of fetching data:
If we have a state that represent the data we fetched from an API, The state will either be one of this three 'states': loading
, data
, or error
When we fetch from an API, Our state will go from loading
( waiting to receive data), to either data
or we'll get an error
Let's compare how we handle state with the useState hook and with the useReducer hook
- With the useState hook:
function Fetcher() {
const [loading, setLoading] = useState(true)
const [data, setData] = useState(null)
const [error, setError] = useState(false)
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/posts/1').then(res => {
setLoading(false)
setData(res.data)
setError(false)
}).catch((err) => {
setLoading(false)
setData(null)
setError(true)
})
,[])
return (
{loading ? <p>Loading...</p>
: <div>
<h1>{data.title}</h1>
<p>{data.body}</p>
</div> }
{error && <p>"An error occured"</p> }
)
}
- With the useReducer hook:
const initialState = {
loading: true,
data: null,
error: false
}
const reducer = (state, action) => {
switch (action.type) {
case "SUCCESS":
return {
loading: false,
data: action.payload,
error: false
};
case "ERROR":
return {
loading: false,
data: null,
error: true
};
default:
return state;
}
}
function Fetcher() {
const [state, dispatch] = useReducer(reducer, initialState)
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/posts/1').then(res => {
dispatch({type: "SUCCESS", payload: res.data})
}).catch(err => {
dispatch({type: "ERROR"})
})
} ,[])
return (
{state.loading ? <p>Loading...</p>
: <div>
<h1>{state.data.title}</h1>
<p>{state.data.body}</p>
</div> }
{state.error && <p>"An error occured"</p> }
)
}
As you can see with the useReducer hook we've grouped the three states together and we also updated them together. useReducer hook is extremely useful when you have states that are related to each other, Trying to handle them all with the useState hook may introduce difficulties depending on the complexity and the bussiness logic of it.
Conclusion
To put it simply: if you have a single state either of a boolean
, number
, or string
use the useState hook. And if you're state is an object or an array, Use the useReducer hook. Especially if it contains states related to each other.
I hope this post was helpful, Happy coding!
Top comments (3)
The problem with reducer is it's required a lot of boilerplate. I usually won't use it unless the state is really complicated and require additional testing.
Thank you for clearing that out!! I was starting to grow an unexplained phobia from useReducer because it looked to mysterious..
In the counter example, ut feels like the advantage of useReducer is that you could move logic like
state-1
andstate+1
into the reducer itself and them simply pass the action type?