useReducer() can serve as a replacement for useState() in scenarios where more powerful state management is required.
useState() is the primary state management hook in React and it works well when state updates are simple and limited to a handful of updates. useReducer() works better when you have related pieces of data/state.
Below is the syntax for calling the Reducer hook:
const [state, dispatchFn] = useReducer(reducerFn, initialState, initialFn)
useReducer returns an array. The first element within this array is a state snapshot used in the component re-render cycle. The second element is a function that can be used to dispatch a new action(trigger an update of the state).
useReducer() takes 3 arguments. reducerFn
is a function that is triggered automatically once an action is dispatched(via dispatchFn
). It receives the latest state snapshot and returns the new, updated state. The second argument is the initial state (this is usually an object). initFn
is a function to set the initial state programmatically.
Let's jump into some code to better understand these concepts
⚔️ useState() vs useReducer() ⚔️
1. Building with useState()
The code below creates an application where clicking on the button fetches 'posts' from a JSONPlaceholder and updates the states(there are three states - loading, post, error).
import React, {useState} from 'react'
function App() {
const [loading, setLoading] = useState(false)
const [post, setPost] = useState({})
const [error, setError] = useState(false)
const handleRequest = () => {
setLoading(true)
setError(false)
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then((res) => {
return res.json()
})
.then((data) => {
setPost(data)
setLoading(false)
})
.catch((err) => {
setError(true)
setLoading(false)
})
}
return (
<div>
<button onClick={handleRequest}>
{loading ? "Wait..." : "Fetch the post"}
</button>
<p>{post?.title}</p>
<span>{error && "Something went wrong!"}</span>
</div>
)
}
export default App
Let's first understand the code.
Inside the return block, we are displaying the post title and an appropriate message if there is an error.
Within handleRequest
, the loading state is set to true. If the request is successful, I am updating the post
state and setting the loading
state as false. If the request failed, we are setting the error
state as true and loading
state as false.
The above example works as expected but on closer inspection you may notice how multiple states are updated together within a single function. Often, in such scenarios, replacing useState() with useReducer() is the preferred approach since we are updating multiple states together.
2. Building with useReducer()
Let's now try understanding how to implement the above application using useReducer().
import React, {useReducer} from 'react'
const postReducer = (state,action) => {
switch(action.type){
case "FETCH_START": return{
laoding: true,
error: false,
post: {}
}
case "FETCH_SUCCESS": return{
loading: false,
error: false,
post: action.payload
}
case "FETCH_ERROR": return{
loading: false,
error: true,
post: {}
}
default: return state
}
}
const INITIAL_STATE = {
loading: false,
post: {},
error: false,
}
function App() {
const [state, dispatch] = useReducer(postReducer, INITIAL_STATE)
const handleRequest = () => {
dispatch({type:"FETCH_START"})
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then((res) => {
return res.json()
})
.then((data) => {
dispatch({type:"FETCH_SUCCESS", payload:data})
})
.catch((err) => {
dispatch({type:"FETCH_ERROR"})
})
}
return (
<div>
<button onClick={handleRequest}>
{state.loading ? "Wait..." : "Fetch the post"}
</button>
<p>{state.post?.title}</p>
<span>{state.error && "Something went wrong!"}</span>
</div>
)
}
export default App
postReducer
is the reducer function that takes two arguments, state
and action
. state
refers to the current state while action
is used to pass information from the component to the reducer function. There are be three kinds of information that we would want to pass -
Case 1 : When the button is clicked, the component informs the reducer that a fetch request is initiated. The reducer then updates the state as follows:
loading - true
post - {}
error - false
Case 2: If is JSON is fetched successfully, the component informs the the reducer and shares the response object. The reducer updates the state as follows:
loading - false
post - {id:1, title: "some_title, desc: some_description}"
error - false
Case 3: If there is an error in fetching the JSON, the component passes this information to the reducer. The reducer then updates the state as follows:
loading - false
post - {}
error - true
The above example illustrates perfectly how useReducer() makes the complex multiple state updation process seamless. 👌.
The reducer hook provides the ability to perform declarative state updates by decoupling state updates from the action that triggered the update.
The dispatch function allows us to send actions to the reducer. The reducer function essentially updates the state and returns the updated version of the state.
If you observe closely, the reducer function is written outside the scope of the component function. The reason behind this is because the Reducer function(postReducer
) does not need to interact with anything defined within the component function. All information required inside the reducer function will be passed by React automatically.
It's completely understandable if the reducer hook got you feeling like this 👇
useReducer() tends to be a complex hook to grasp but understanding its functionality will go a long way when learning Redux.
Top comments (2)
thanks @mihir_chhatre
Glad it helped :')