Anatomy of useReducer Hook: React JS
Demystifying React’s useReducer Hook: Understanding its Inner Workings
In this article we will be learning about useReducer hook and how it can help us to solve the classic problem of useState
hell.
Introducing the useReducer Hook
What is it?
Just another inbuilt React Hook.
What is it’s purpose?
To make it clear: It’s purpose is mimic the behaviour of Redux. Provides a mechanism to handle state using Reduxi way.
In technical terms: The useReducer
hook in React provides a structured way to manage state by handling state transitions through a reducer function, making complex state logic and interdependencies more organized and predictable. It's an alternative to the useState
hook, suitable for scenarios where state management becomes intricate.
What is the syntax of using it?
// Basic Example:
const [state, setState] = useReducer(reducerFunction, {name: ''});
function reducerFunction(state) {
return {name: 'XYZ'};
}
function onInput() {
setState();
}
// Detailed Example: Extend to use Redux things.
const initialState = {name: '', age:23};
const [myState, setMyState] = useReducer(myReducerFunction, initialState);
function myReducerFunction(previousState, action) {
// we have to return state from this function
.
.
// your logic lies here which can modify the state
const {type, payload} = action;
switch (type) {
case 'INCREMENT':
return { ...previousState, count: state.count + payload.incrementBy};
case 'DECREMENT':
return { ...previousState,count: state.count - 1 };
default:
return previousState;
}
}
function handleClick() {
setMyState({
type: 'Increment',
payload: {incrementBy: 9}
});
}
Let’s understand each line:
When we call useReducer
hook, it takes 2 arguments and returns an array with two values.
First argument is a callback function, which get’s called when we invoke the setter method.
Second argument is the
initial
state.
In the returned array,
At first Index we get our
updated state
and in second index we get a function which is setter function, just like we get one inuseState
.
Things to note: It’s not necessary to pass object as initial state, but generally if we are working with useReducer object is advised because most probably we are going to deal with complex state
What is useState hell?
Let’s see it first:
Trust me this code snippet is from production app. Now if you are working in the code base it might be easy for you to understand the states.
But for a developer who is new to code base, it will be very challenging for him to track these updates at first glance.
Now this is simple, some developer wirte useState calls in between functions, those are more complex.
The term “useState hell” refers to a situation in React development where the excessive use of the useState
hook for managing state results in complex, convoluted, and difficult-to-maintain code.
This situation can lead to several problems and challenges:
No Guards:
In useState calls, when we have many of them, it becomes difficult to update state based on certain conditions, and if one state update depends on value of other state then it becomes more difficult to track those states.
Code Complexity:
When a component’s state management relies heavily on useState
, the code can quickly become complex. Multiple useState
calls and their associated logic can lead to nested structures and convoluted updates.
Difficulty in Understanding:
The more useState
calls you have, the harder it becomes to understand how different pieces of state are interconnected and how they influence each other. This can lead to confusion for developers who need to work with or maintain the code.
Interdependent State: In many cases, the state of one piece of your component might depend on the state of another piece. When this interdependence becomes intricate, managing it using separate useState
calls can lead to difficult-to-trace bugs and unexpected behavior.
Readability: Excessive useState
calls can clutter your component's codebase, making it challenging to identify the main logic and rendering components less readable.
Debugging Complexity: Debugging becomes challenging when state updates are scattered across different parts of the component. Identifying the source of an issue and tracking state changes becomes more time-consuming.
Performance Impact: Each useState
call triggers a re-render of the component. When you have numerous state updates dispersed throughout the component, you might experience performance issues due to unnecessary re-renders.
Refactoring Difficulty: As your component evolves, you might need to refactor or extend its functionality. In “useState hell,” refactoring becomes riskier due to the complex interactions between different state variables.
Maintenance Burden: As your codebase grows, maintaining a component with excessive useState
calls can become a significant burden. Modifying or extending the component's behavior becomes harder, potentially introducing errors.
For such situation useReducer can be a saviour.
By using useReducer, we can have cenralized logic for complex states used in our component.
Here is an example:
import React, { useReducer } from 'react';
const initialState = {
count: 0,
isLightOn: false,
userName: '',
todos: [],
isLoading: false,
showModal: false,
};
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'TOGGLE_LIGHT':
return { ...state, isLightOn: !state.isLightOn };
case 'UPDATE_NAME':
return { ...state, userName: action.payload };
case 'ADD_TODO':
return { ...state, todos: [...state.todos, action.payload] };
case 'TOGGLE_LOADING':
return { ...state, isLoading: !state.isLoading };
case 'TOGGLE_MODAL':
return { ...state, showModal: !state.showModal };
default:
return state;
}
};
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
<div>
Light: {state.isLightOn ? 'On' : 'Off'}
<button onClick={() => dispatch({ type: 'TOGGLE_LIGHT' })}>Toggle Light</button>
</div>
<div>
User: {state.userName}
<input
type="text"
value={state.userName}
onChange={(e) => dispatch({ type: 'UPDATE_NAME', payload: e.target.value })}
/>
</div>
<div>
Todos: {state.todos.join(', ')}
<button onClick={() => dispatch({ type: 'ADD_TODO', payload: 'New Todo' })}>Add Todo</button>
</div>
<div>
Loading: {state.isLoading ? 'Yes' : 'No'}
<button onClick={() => dispatch({ type: 'TOGGLE_LOADING' })}>Toggle Loading</button>
</div>
<div>
Modal: {state.showModal ? 'Open' : 'Closed'}
<button onClick={() => dispatch({ type: 'TOGGLE_MODAL' })}>Toggle Modal</button>
</div>
</div>
);
}
export default App;
Other use cases where useReducer can be beneficial:
Predictable State Updates
With useReducer
, the state transitions are handled through a reducer function that takes the current state and an action as arguments and returns the next state. This ensures that the state transitions are explicit and predictable.
Interdependent State Changes
If one state update depends on another state update, you can ensure proper sequencing of state transitions within the reducer function. This can prevent race conditions or inconsistent updates that might occur with separate useState
calls.
Global State Management:
While not a full replacement for more advanced state management solutions like Redux or MobX, useReducer
can be used to manage global state within a component or a subtree of the component tree, which can be particularly useful for smaller-scale applications.
Performance Optimization: (Batched Updates)
When state updates depend on previous state values, useState
might lead to performance issues due to multiple consecutive updates. useReducer
allows you to optimize updates by batching them within a single call.
Since useReducer
batches state updates within a single dispatch, it can help prevent unnecessary re-renders that might occur when using multiple useState
calls. This optimization can improve the performance of your component.
Debugging and Logging:
The use of a reducer function centralizes state changes, making it easier to log and debug state transitions, especially when the application grows in complexity.
Should I stop using Redux then?
No, redux exists before we got the useReducer hook, redux dev tools is awesome where you can visualise your state and do time travelling debugging.
useReducer is not a replacement for redux.
Instead we can use useReducer in certain sections of our application where we don’t need redux. For example: Side Drawer States, Form States, Dialog states etc.
Thanks for reading. Let me know in comments if you have any other use case of it.