useReducer Hook
Objectives
- Discuss the concept of a
reducer
in JavaScript - Implement a basic example of a
reducer
- Implement a basic example of the
useReducer
hook - Explain when and where to use
useState
vsuseReducer
What is a reducer?
A reducer
is a fancy word for a function that takes 2 values and returns 1 value back.
In fact you may have already worked with a reducer
it you have ever used the Array.reduce()
method.
Array.reduce((acc, val) => {
return acc;
}, init);
The parameters that reduce()
takes in are:
- a callback function that takes in an accumulator (acc) and the current element in the loop (val)
- an optional starting value (init)
The accumulator
will determine if it has been assigned a starting value otherwise it will use the element at the first position in the array.
Working Through Some Examples
Let's work through a few examples of Array.reduce()
so we are all up to speed on what the method is meant to do.
Starter Repl: reduce-examples-starter repl
Sum An Array
How would you use .reduce()
to return a single value that is the sum of all numbers in the following array?
INPUT: [1, 2, 3];
OUTPUT: 6;
Counting Duplicates
Another use case for .reduce()
would be to count duplicates. Say you had the following and wanted to count how many instances there were of each.
INPUT: ['banana', 'cherry', 'orange', 'apple', 'cherry', 'orange', 'apple', 'banana', 'cherry', 'orange', 'fig' ]
OUTPUT: { banana: 2, cherry: 3, orange: 3, apple: 2, fig: 1 }
In both cases we reduced the input of many things to a single thing, be it a number or an object.
❓ In your own words explain why you would use [].reduce()
vs [].forEach
or even a for()
loop?
Creating A Reducer
So now that we have worked through a few examples of [].reduce()
let's apply this knowledge and create a reducer
.
For this small demo we will be using the following starter code:
So the concept of a reducer
has been around for sometime in JavaScript long before the introduction of Array.reduce
.
When applied to building an application it becomes a tool which we use to manage both the state
of an application and thebusiness logic
as well.
So the reducer
is essentially a function that does the following:
takes in the following params
- current state
- the action to be performed on state
returns
- a new/updated version of state (old state is never mutated)
const myReducer = (state, action) => {
// returns a new version of state
};
This follows one of the rules we learned regarding state which is:
🚔 Never update the state value directly
Although we already have a working example of a Counter
component, lets give the code a once over so were all on the same page.
Here is the state of the Counter.
const [count, setCount] = useState(0);
Here are our supporting functions
const handleIncrement = () => setCounter(count + 1);
const handleDecrement = () => setCounter(count - 1);
const handleReset = () => setCounter(0);
And of course the buttons that call the supporting functions.
<section>
<h2>Count:{count}</h2>
<button onClick="{handleIncrement}">+</button>
<button onClick="{handleDecrement}">-</button>
<button onClick="{handleReset}">Reset</button>
</section>
;
Our First Reducer
Let's refactor this a bit to use a reducer
function. The idea here is to aggregate state and all the logic update state into one single function.
It will take in the following:
- current state
- an action to perform that will update state
const counterReducer = (state, action) => {
if (action === "INCREMENT") {
return state + 1;
} else if (action === "DECREMENT") {
return state - 1;
} else if (action === "RESET") {
return 0;
}
return state;
};
One thing to note about the above code is that the action
being passed is expected, by convention, to be uppercase. This convention is meant to highlight the action being performed and a convention that is expected when using Redux
.
If the action
doesn't match any condition, we default to return the unchanged state. It's very clear in the function that the action
determines how state is to be updated.
Refactor Buttons
And of course the buttons need a bit of refactoring as well. Once again we are passing in the current state and the action to be performed.
<button onClick={() => setCount(counterReducer(count, "INCREMENT"))}>
+
</button>
<button onClick={() => setCount(counterReducer(count, "DECREMENT"))}>
-
</button>
<button onClick={() => setCount(counterReducer(count, "RESET"))}>
Reset
</button>
Switch Statements
For the sake of readability switch
statements have become the defacto conditional logic for reducers.
So let's rewrite the above code as follows:
const counterReducer = (state, action) => {
switch (action) {
case "INCREMENT":
return state + 1;
case "DECREMENT":
return state - 1;
case "RESET":
return 0;
default:
return state;
}
};
From the looks of it the switch statement does indeed make it easier to read.
❓ What are the benefits of managing the applications state using a reducer
function?
useReducer Hook II
The two basic hooks that are used for state management in React are: useState
and useReducer
, with the addition of useContext
for managing a more global form of state.
In order to work with the useReducer
hook we need to first import it and, since it essentially replaces useState
we can remove that.
import React, { useReducer } from "react";
useReducer
works very similar to useState
but with some differences. Like useState it returns a tuple [state, setState]
with the first element in the array being state
and the second a set function
It almost seems like the two are the same at this point. But there is a difference.
useReducer
takes in a callback
as the first argument and the initial state value
as the second.
Another convention to follow is that the setState
function is called dispatch
.
// const [count, dispatch] = useReducer( callback function, initial state)
const [count, dispatch] = useReducer((state, action) => {}, 0);
In order to better convey the dispatch
naming convention let's take a look at the reducer in dev tools:
console.log(
"Counter - useReducer(counterReducer, 0)",
useReducer(counterReducer, 0)
);
We should see the following and take note of the fact that the target function is called dispatchAction
.
Using the counterReducer
Function
With useReducer
in place we can now replace the callback with the counterReducer
function that we created earlier
const [count, dispatch] = useReducer(counterReducer, 0);
Now all that is left is to update are the buttons. Although dispatch
is essentially the counterReducer
function, which itself takes in two params: state
and action
, we only need to pass dispatch a single action
value.
This is because useReducer
will be executing the callback function and it takes on the responsibility of managing the current state and updating it based on the action.
<button onClick={() => dispatch("INCREMENT")}>+</button>
<button onClick={() => dispatch("DECREMENT")}>-</button>
<button onClick={() => dispatch("RESET")}>Reset</button>
Working With More Complex Actions
If choosing to go with useReducer
there's a good chance your working with a more complex version of state object and/or which require more action values.
Action
The convention for writing an Action
is to have both a type
and a payload
. While the type is the action to be performed, the payload is the value used to update state.
Our first refactor is to send an object as a payload which includes the type
of action to perform and the value
by which to update state.
<button onClick={() => dispatch({type: "INCREMENT", value: 1})}>+</button>
<button onClick={() => dispatch({type: "DECREMENT", value: 1})}>-</button>
<button onClick={() => dispatch({type: "RESET", value: 0})}>Reset</button>
Our second refactor is on counterReducer and here we update the code to reference either action.type
or action.value
.
const counterReducer = (state, action) => {
switch (action.type) {
case "INCREMENT":
return (state += action.value);
case "DECREMENT":
return (state -= action.value);
case "RESET":
return (state = action.value);
default:
return state;
}
};
Final Solution Code
Here is the final solution code:
CodeSandbox - Counter Reducer - Solution - Provided after the lecture
❓ How does working with useReducer
improve the readability and organization of our code?
Bonus Material
Why useReducer over useState?
Whenever there are two things that seem do the same thing, people inevitably ask: "When do I use one over the other?" Since we have useState, why do we need useReducer at all?
The React team advises the following:
useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one. useReducer also lets you optimize performance for components that trigger deep updates because you can pass dispatch down instead of callbacks . — React Team
Also Kent C. Dodds wrote up a explanation (article linked in references) of the differences between the two and, while he often reaches for setState, he provides a good use case for using useReducer instead:
If one element of your state relies on the value of another element of your state, then it’s almost always best to use useReducer
The example he works through in his article is a bit advanced but the gist of it is that he is implementing a state that includes a past
, present
and future
and is a more complex version of state than we are used to working with.