Introduction
useReducer is a React Hook introduced late in October 2018, which allows us to handle complex state logic and action. It was inspired by Redux state management pattern and hence behaves in a somewhat similar way.
Oh! but don't we already have a useState hook to handle state management in React?
Well, yes! useState does the job pretty well.
However,
the useState hook is limited in cases where a component needs a complex state structure and proper sync with the tree. useReducer when combined with useContext hook could behave very similarly to Redux pattern and sometimes might be a better approach for global state management instead of other unofficial libraries such as Redux.
As a matter of fact, the useReducer's API itself was used to create a simpler useState hook for state management.
According to React's official docs :
"An alternative to useState. Accepts a reducer of type (state, action) => newState, and returns the current state paired with a dispatch method."
A call to Reduce Method in JavaScript
To begin with useReducer, first, we need to understand how JavaScript's built-in Array method called Reduce works, which shares remarkable similarity with the useReducer hook.
The reduce method calls a function (a reducer function), operates on each element of an array and always returns a single value.
function reducer(accumulator, currentvalue, currentIndex, sourceArray){
// returns a single value
}
arrayname.reduce(reducer)
As stated, the above reducer function takes in 4 parameters -
1. Accumulator : It stores the callback return values.
2. Current Value : The current value in the array being processed.
3. Current Index (optional) : The index of the current value in the array being processed.
4. Source Array : The source of the array on which reduce method was called upon.
Let's see reduce function in action, by creating a simple array of elements :
const items = [1, 10, 13, 24, 5]
Now, We will create a simple function called sum
, to add up all the elements in the items array. The sum
function is our reducer function, as explained above in the syntax
const items = [1, 10, 13, 24, 5]
function sum(a,b, c, d){
return a + b
}
As we can see, I am passing four parameters named a, b, c, d
, these parameters can be thought of as Accumulator, Current Value, Current Index, Source Array
respectively.
Finally, calling the reduce
method on our sum
reducer function as follows
const items = [1, 10, 13, 24, 5];
function sum(a, b, c, d){
return a + b;
}
const out = items.reduce(sum);
console.log(out);
OUTPUT :
59
Let's understand what's going on here :
When the reduce
method gets called on the sum
function, the accumulator ( here a
) is placed on the zeroth index (1), the Current Value (here b) is on 10
. On the next loop of the sum
function, the accumulator adds up a + b
from the previous iteration and stores it up in accumulator
(a) while the current value(b) points to 13 now.
Similarly, the accumulator keeps on adding the items from the array whatever the Current Index is pointing until it reaches the very end of it. Hence resulting in the summation of all the items in the array.
// We can do a console.log to check iteration in every function loop :
const items = [1,10,13,24,5];
function sum(a, b, c, d){
console.log("Accumulator", a)
console.log("Current Index", b)
return a + b
}
const out = items.reduce(sum);
console.log(out);
'Accumulator' 1
'Current Index' 10
'Accumulator' 11
'Current Index' 13
'Accumulator' 24
'Current Index' 24
'Accumulator' 48
'Current Index' 5
53
In addition to this, there is an optional initial value
, when provided will set the accumulator to the initial value first, before going out for the first index item in the array. The syntax could look like this :
items.reduce(sum, initial value)
While we just finished understanding how the reduce
method works in JavaScript, turns out both the Redux library and the useReducer
hook shares a common pattern, hence the similar name.
How does useReducer works?
useReducer expects two parameters namely, a reducer function and an initial state.
useReducer(reducer, initialState)
Again the reducer function expects two parameters, a current state and an action and returns a new state.
function reducer(currentState, action){
returns newState;
}
useReducer Hook in Simple State and Action
Based on what we have learnt so far, let's create a very simple counter component with increment, decrement feature.
We will begin by generating a JSX component :
import React from 'react';
import ReactDOM from 'react';
function App(){
return (
<div>
<button onClick={handleIncrement}>+</button>
<span>{state}</span>
<button onClick={handleDecrement}>-</button>
</div>
);
}
// define a root div in a HTML file and mount as such
ReactDOM.render(<App />, document.getElementById("root"));
Create a reducer
function, expecting a state and an action. Also, attach onClick
events on both buttons and define the click
functions within the App
component :
import React, {useReducer} from 'react';
import ReactDOM from 'react';
function reducer(state, action){
// return newState
}
function App(){
function handleDecrement() {
// ...
}
function handleIncrement() {
// ...
}
return (
<div>
<button onClick={handleIncrement}>+</button>
<span>{state}</span>
<button onClick={handleDecrement}>-</button>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
Moving onwards, before we trigger useReducer
hook, it is important to note that it returns an array of two values,
const state = useReducer[0]
const dispatch = useReducer[1]
We can simplify the above, using array destructuring (a best practice) :
const [state, dispatch] = useReducer(reducer, intialState)
Now, coming back to our counter component and including the above useReducer
snippet in it
function reducer(state, action){
if (action === "increment") {
return state + 1;
}
else if (action === "decrement") {
return state - 1;
}
else null;
}
function App(){
function handleDecrement() {
dispatch("decrement");
}
function handleIncrement() {
dispatch("increment");
}
const [state, dispatch] = React.useReducer(reducer, (initialState = 2));
return (
<div>
<button onClick={handleIncrement}>+</button>
<span>{state}</span>
<button onClick={handleDecrement}>-</button>
</div>
);
}
Link to codepen
The handleIncrement
and handleDecrement
function returns a dispatch method with a string called increment
and decrement
respectively. Based on that dispatch method, there is an if-else statement in the reducer function which returns a new state and eventually triggering (overriding in a way) the state
in useReducer.
According to the official docs, always use Switch
statements in the reducer function (you already know this if you have worked with Redux before), for more cleaner and maintainable code. Adding more to this, it is advisable to create an initial state object and pass a reference to the useReducer
const initialState = {
count: 0
// initialize other data here
}
const [state, dispatch] = React.useReducer(reducer, intialState);
useReducer Hook in Complex State and Action
Let's see the same counter component, building it with what we have learnt so far but this time with a little complexity, more abstraction, also best practices.
const initialState = {
count: 0
};
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + action.value };
case "decrement":
return { count: state.count - action.value };
}
}
function App() {
const [state, dispatch] = React.useReducer(reducer, initialState);
return (
<div>
<button onClick={() => dispatch({ type: "increment", value: 5 })}>
+
</button>
<span>{state.count}</span>
<button onClick={() => dispatch({ type: "decrement", value: 5 })}>
-
</button>
</div>
);
}
Link to codepen
What has changed?
Instead of passing a value directly to the
useReducer
hook, we have an object initialised with a count property set to zero. This helps in cases when there are more than a single property to be initialised also easier to operate on an object.As we discussed earlier,
if-else
has been modified toswitch
based statements in the reducer function.The
dispatch
method is now object-based and provides two propertiestype
andvalue
. Since the dispatch method triggersaction
, we can switch statements in the reducer function usingaction.type
. Also, the new state can be modified by using a dynamic value which can be accessed onaction.value
Dealing with Multiple useReducer Hook
When dealing with multiple state variables that have the same state transition, sometimes it could be useful to use multiple useReducer
hook that uses the same reducer
function.
Let's see an example :
const initialState = {
count : 0
}
function reducer(state, action) {
switch (action) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default :
return state
}
}
function App() {
const [state, dispatch] = React.useReducer(reducer, initialState);
const [stateTwo, dispatchTwo] = React.useReducer(reducer, initialState);
return (
<>
<div>
<button onClick={() => dispatch('increment')}>+</button>
<span>{state.count}</span>
<button onClick={() => dispatch('decrement')}>-</button>
</div>
<div>
<button onClick={() => dispatchTwo('increment')}>+</button>
<span>{stateTwo.count}</span>
<button onClick={() => dispatchTwo('decrement')}>-</button>
</div>
</>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
Here we are using two useReducer
hook with different dispatch and state triggering the same reducer
function.
Link to codepen
useState v/s useReducer
Here is a quick rundown, comparing both the hooks :
useReducer's Rendering Behavior
React renders and re-renders any useReducer
component very similar to the useState
hook.
consider the following contrived example where +
increments the count
by 1, -
decrements the count
by 1 and Reset
restores the count
value to 0.
function App(){
const [count, dispatch] = useReducer(reducer, initialState)
console.log("COMPONENT RENDERING");
return (
<div>
<div>{count}</div>
<button onClick={() => {dispatch('increment')}>+</button>
<button onClick={() => {dispatch('decrement')}>-</button>
<button onClick={() => dispatch('reset')}>Reset</button>
</div>
)
}
The above App
component :
1. Re-render every time the count
state changes its value, hence logging out the COMPONENT RENDERING
line.
2. Once, the reset button has been clicked, the subsequent clicks to the reset
button won't re-render the App
component as the state value is always zero.
While we just finished reading how rendering happens in the context of useReducer
, we have reached the end of this article!
Some Important Resources that I have collected over time:
1. https://reactjs.org/docs/hooks-reference.html#usereducer
2. https://geekflare.com/react-rendering/
3. https://kentcdodds.com/blog/should-i-usestate-or-usereducer
4. https://kentcdodds.com/blog/application-state-management-with-react
Loved this post? Have a suggestion or just want to say hi? Reach out to me on Twitter
Originally written by Abhinav Anshul for JavaScript Works
Top comments (1)
Great explanation man! ❤️👊