Last time we looked into React Hooks and why they've been introduced, then we refactored a react component built as a class into a function component using hooks, allowing us to share logic between components. Now we're going to look at managing complex state with useState and useReducer.
See the code in this blog in action at
Counter with useState and useReducer
React useState hook
useState is a basic hook that allows us to have local state in a functional component.
We create one by declaring a state variable: counter followed by a function to update it: setCounter.
Initialize the count to zero by passing it 0 as the only argument to useState().
Lastly, because we want to update our state by adding 1 to the previous state(initialized at 0), we need to provide a function of the previous state as an argument to update it.
If you're setting a value that does not rely on the value of the old state, then you can use a value as the argument, otherwise, use a function.
function Counter() {
const [counter, setCounter] = useState(0)
const increment = () => setCounter(counter + 1)
return (
<div>
<div>{counter}</div>
<button type="button" onClick={increment}>
+
</button>
</div>
)
}
Notice counter (value) and setCounter are being returned as an array from useState(). The order here is important, The setter function will control the state that is managed by the hook.
π€π€ What happens when we want another piece of state?
function Counter() {
const [count, setCount] = useState(0)
const [disabled, setDisabled] = useState(false)
const increment = () => setCount(count + 1)
const toggleDisabled = () => setDisabled(!disabled)
return (
<div>
<div>{count}</div>
<button type="button" disabled={disabled} onClick={increment}>
+
</button>
<button type="button" onClick={toggleDisabled}>
{disabled ? "enable" : "disable"}
</button>
</div>
)
}
We can do it as many times as we want! Each useState is unique and can function independently of each other.
However, after writing your third or fourth useState in the same component, you start to feel like that there's got to be something better. If your component needed the ability to undo, you'd have to keep track of the previous state ontop of the current state for every operation.
Let's add some extra capabilities to our counter by adding the ability to decrement, reset, and undo.
function Counter({ initialValue = 0 }) {
const [previousCount, setPreviousCount] = useState(null)
const [count, setCount] = useState(0)
const increment = () => {
setPreviousCount(count)
setCount(count + 1)
}
const decrement = () => {
setPreviousCount(count)
setCount(count - 1)
}
const reset = () => {
setPreviousCount(null)
setCount(initialValue)
}
const undo = () => {
setCount(previousCount)
setPreviousCount(null)
}
const toggleDisabled = () => setDisabled(!disabled)
return (
<div>
<div>{count}</div>
<button type="button" onClick={increment}>
+
</button>
<button type="button" onClick={decrement}>
-
</button>
<button type="button" onClick={reset}>
Reset
</button>
<button type="button" onClick={undo} disabled={!prevValue}>
Undo
</button>
</div>
)
}
Hrmm. We did it, but that's a lot of slightly different, but mostly repetitive, code.
React useReducer hook π₯π₯π₯
What is useReducer ? It's a hook based off of Javascript's very popular reduce function. Definitely a topic that will require it's own blog post eventually, but until then, here are a few outstanding posts by other people on it
Sarah Drasner: The Almighty Reducer
Kent C Dodds: Array reduce vs chaining vs for loop
An important takeaway of the reducer is it will always return one value, By reducing the previous value down to a single representative one. Reducers are incredibly powerful and it's hard to get far on the internet without seeing them referenced.
The reducer function is a function that receives a state and an action, then reduces to a new state.
(state, action) => newState
The useReducer hook is an alternative to useState, meaning you can use the two interchangeably. However, useReducer is preferred over useState when you have complex state logic, especially when the next state is dependant on the previous state.
useReducer takes two arguments: First being the reducer() method itself and the second is a dispatch method that calls the reduer() method.
The useReducer hook looks like this. It's a function that receives a state and an acation, and reduces it to a new value, in our case: state.
const [state, dispatch] = useReducer(reducer, initialState)
Let's rebuild the previous Counter component using useReducer instead of useState
const CounterReducer = ({ initialState = 0 }) => {
const reducer = (state, action) => {
switch (action.type) {
case "increment":
return {
count: state.count + 1,
prevValue: state.count,
}
case "decrement":
return {
count: state.count - 1,
prevValue: state.count,
}
case "reset":
return {
count: initialState,
prevValue: null,
}
case "undo":
return {
count: state.prevValue,
prevValue: null,
}
default:
return state
}
}
const [state, dispatch] = useReducer(reducer, {
count: initialState,
prevValue: null,
})
return (
<div>
<h1>useReducer Counter: {state.count}</h1>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "reset" })}>Reset</button>
<button
onClick={() => dispatch({ type: "undo" })}
disabled={!state.prevValue}
>
Undo
</button>
</div>
)
}
Nice! And it looks pretty similar to the architecture used in popular state management library, Redux.
Instead of using a bunch of useState updater functions, useReducer centralizes the state logic of the component into a single reducer function.
Our lord and savior Dan Abramov summed up in a tweet of when to use useState or useReducer when managing component state:
When to use useReducer
Should I use multiple useState or useReducer?
β Dan Abramov (@dan_abramov) January 10, 2019
For independent things (isHovering and textInput), multiple useState.
For things that change together (isFetching and fetchedItems), or if their next state depends on previous (todos), I prefer useReducer.https://t.co/wSyKITMRpZ
For me, I use useState until it feels like the components stateful logic is overly complex. Forms and CRUD applications are a great example of when you'd benefit from useReducer over useState. We'll build a todo-list using useReducer next time.
Recap
useReducer>useStatewhen handling complex state logic that involves multiple sub values, or when the next state is reliant on the previous stateWhen called,
useReducerwill return an array of two items: Current State and a Dispatch method-
useReduceraccepts three arguments:- reducer function
- initial state
- third, optional init function for lazy initialization of state
Reducers are responsible for handliong transitions from one state to the next. They take in current state and an action and return a new state
Actions are unique events that happen in your app
Actions are dispatched to our reducer when specific events take place.
Hot Damn that was a long post. Thanks for reading!
this was originally posted April 22, 2020 at Hooks-and-Hamburgers.com


Top comments (0)