The useReducer
hook is an alternative to useState
for managing state in React functional components. It is particularly useful for complex state logic involving multiple sub-states or when the next state depends on the previous state.
useState
and useReducer
Feature | useState | useReducer |
---|---|---|
Complexity | Ideal for simple state logic | Suitable for complex state logic |
State Updates | Directly set state via updater | Use a reducer function to determine state changes |
Structure | Single piece of state | State + Dispatch + Reducer Function |
Scalability | Can become messy with multiple states | Organizes updates into a single function |
useReducer
Over useState
useState
to useReducer
Let’s refactor an example where a simple word counter with delete and reset actions evolves from useState
to useReducer
.
useState
Here’s a counter example with useState
:
import React, { render, useState } from "react";
const CounterWithState = () => {
const [count, setCount] = useState(0);
const [word, setWord] = useState("");
function changeHandler(e) {
setWord(e.currentTarget.value);
setCount(e.currentTarget.value.length);
}
function del() {
if (word.length) {
setWord(word.slice(0, -1));
setCount(count - 1);
}
}
function reset() {
setWord("");
setCount(0)
}
return (
<div>
<h1>Counter with useState</h1>
<p>Letters: {count}</p>
<input value={word} onChange={changeHandler} />
<button onClick={del}>Delete</button>
<button onClick={reset}>Reset</button>
</div>
);
};
render(<CounterWithState />)
useReducer
Here, we replace useState
with useReducer
for a cleaner and more scalable approach.
The reducer function determines how state changes based on an action
.
type State = {
word: string,
count: number
}
type Action =
| { type: 'WORD', value: string }
| { type: 'DELETE' }
| { type: 'RESET' }
const reducer = (state: State, action: Action) => {
switch (action.type) {
case "WORD":
return { count: action.value.length, word: action.value };
case "DELETE":
return state.word.length
? { count: state.count - 1, word: state.word.splice(0, -1) }
: state;
case "RESET":
return { word: '', count: 0 };
default:
throw new Error("Action not handled");
}
};
Here’s the updated counter using useReducer
:
Copy code
import React, { useReducer } from "react";
const CounterWithReducer = () => {
const [state, dispatch] = useReducer(reducer, { count: 0, word: "" });
return (
<div>
<h1>Counter with useState</h1>
<p>Letters: {state.count}</p>
<input value={state.word} onChange={(e) => dispatch('WORD', e.currentTarget.value)} />
<button onClick={() => dispatch('DELETE')}>Delete</button>
<button onClick={() => dispatch('RESET')}>Reset</button>
</div>
);
};
export default CounterWithReducer;
type
.{ count: 0, word: “” }
is passed as the initial state to useReducer
.setState
, we call dispatch
with an action
object.useReducer
Over useState
:reducer
function.Feature | useState Example | useReducer Example |
---|---|---|
State Structure | Single state variable | Centralized state object |
Update Logic | Spread across multiple setState calls | Encapsulated in the reducer function |
State Updates | Direct calls to setState | dispatch triggers state updates |
useState
is great for simple scenarios with minimal state logic.useReducer
is a powerful tool for managing complex state logic and actions.This refactoring demonstrates how useReducer
provides better structure, cleaner code, and enhanced scalability for complex state management. Let me know if you'd like a more advanced example! 🚀
The useReducer hook is an alternative to useState for managing state in React functional components. It is particularly useful for complex state logic involving multiple sub-states or when the next state depends on the previous state.
With useState you have to define a state update function per each state variable. With useReducer, you use an independent reducer function to determine state changes. More on this later.
useState hold only a single piece of state, while useReducer defines the state, the dispatch and reducer function. A little more to worry about but simplifies things in complex states.
Last, with a complex state useState can become messy with multiple setState functions. Also, it is difficult to test the functionality of state change. With useReducer, all state changes are neatly organised into a single function.
Let’s refactor an example with a simple word counter delete, and reset actions evolve from useState to useReducer.
We use two state variables, word and count, holding the word length.
In the change handler, we update both state variables
In the delete handler we remove one letter from the end if possible and update count
The reset handler resets to an empty word.
The form provides the text input and buttons to handle delete and reset.
Already, we can see that adding many other handlers can lead to spaghetti code and confusion. Let's simplify this!
Let's convert the useState example to useReducer. We make sure everything is type safe and first we define the shape of our state, containing the word and count of its letters.
Then, we define all the actions we want our reducer to support. As you add more functionality, you add a new function name and shape of the passed parameters.
Then, we define the reducer, which accepts two parameters: the current state and the action being dispatched. The action contains two properties: the type or name of the action and the value, which is the payload of the action. Now, we onlyt have to implement a functionality for each action type, in our case ...
...changing the word and recalculating word length ...
... deleting the last letter ...
...resetting the word ...
... and also checking if we handled all the possible cases. Typescript is very helpful in this case, as dispatching a wrong action name shows compile time error.
The form now becomes much simpler. Firsr, we initialise the state and dispatch dispatch function using the useReducer hook.
Last, instead of calling setState, we dispatch the correct action identified by type with the required value.
Overall, useReducer prides several advantages over useState, but it's up to you if you decide to use it.
useReducer delivers cleaner state management for complex logic.
Centralized state transitions in a single reducer function.
Easier to debug and test state logic independently on your component
Scales much better when adding more actions or state properties.