The useReducer hook is an advanced React hook used for managing complex state logic in a functional component. It is an alternative to useState and is particularly useful when the state depends on previous states or when multiple state transitions are required.
When to Use useReducer
- When state logic is complex and involves multiple sub-values.
- When the next state depends on the previous state.
- When the logic for state updates is centralized and reusable (e.g., in a reducer function).
- Useful in managing state for applications like form handling, complex toggles, or implementing state machines.
Syntax
const [state, dispatch] = useReducer(reducer, initialState);
reducer: A function that specifies how the state transitions happen. It takes the currentstateand anaction, and returns the updated state.initialState: The initial state value.state: The current state managed by theuseReducerhook.dispatch: A function to trigger state transitions.
Reducer Function Structure
function reducer(state, action) {
switch (action.type) {
case "ACTION_TYPE_1":
return { ...state, property: action.payload };
case "ACTION_TYPE_2":
return { ...state, anotherProperty: action.payload };
default:
return state; // Return the current state if no matching action is found.
}
}
Basic Example: Counter
import React, { useReducer } from "react";
function Counter() {
const initialState = { count: 0 };
const reducer = (state, action) => {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
case "reset":
return { count: 0 };
default:
return state;
}
};
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: "increment" })}>Increment</button>
<button onClick={() => dispatch({ type: "decrement" })}>Decrement</button>
<button onClick={() => dispatch({ type: "reset" })}>Reset</button>
</div>
);
}
export default Counter;
Example: Form Handling
import React, { useReducer } from "react";
const initialState = {
username: "",
email: "",
password: "",
};
function reducer(state, action) {
switch (action.type) {
case "updateField":
return { ...state, [action.field]: action.value };
case "reset":
return initialState;
default:
return state;
}
}
function FormExample() {
const [state, dispatch] = useReducer(reducer, initialState);
const handleChange = (e) => {
dispatch({
type: "updateField",
field: e.target.name,
value: e.target.value,
});
};
const handleReset = () => {
dispatch({ type: "reset" });
};
return (
<form>
<input
name="username"
value={state.username}
onChange={handleChange}
placeholder="Username"
/>
<input
name="email"
value={state.email}
onChange={handleChange}
placeholder="Email"
/>
<input
name="password"
type="password"
value={state.password}
onChange={handleChange}
placeholder="Password"
/>
<button type="button" onClick={handleReset}>
Reset
</button>
<p>Form State: {JSON.stringify(state)}</p>
</form>
);
}
export default FormExample;
Key Concepts of useReducer
- Centralized State Management:
useReducercentralizes all state logic in the reducer function, making it easier to manage complex states. - Action-Based Updates: State changes are triggered by
dispatching an action. Actions have atypeand often apayload. - Immutability: Always return a new state object in the reducer. Do not directly modify the current state.
Example: Todo App
import React, { useReducer } from "react";
const initialState = [];
function reducer(state, action) {
switch (action.type) {
case "add":
return [...state, { id: Date.now(), text: action.payload, completed: false }];
case "toggle":
return state.map((todo) =>
todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo
);
case "remove":
return state.filter((todo) => todo.id !== action.payload);
default:
return state;
}
}
function TodoApp() {
const [todos, dispatch] = useReducer(reducer, initialState);
const [text, setText] = React.useState("");
const handleAdd = () => {
if (text.trim()) {
dispatch({ type: "add", payload: text });
setText("");
}
};
return (
<div>
<h3>Todo List</h3>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Add a todo"
/>
<button onClick={handleAdd}>Add</button>
<ul>
{todos.map((todo) => (
<li key={todo.id} style={{ textDecoration: todo.completed ? "line-through" : "" }}>
<span onClick={() => dispatch({ type: "toggle", payload: todo.id })}>
{todo.text}
</span>
<button onClick={() => dispatch({ type: "remove", payload: todo.id })}>
Remove
</button>
</li>
))}
</ul>
</div>
);
}
export default TodoApp;
Key Points
- Better for Complex State Logic: Prefer
useReduceroveruseStatewhen the state involves multiple transitions or dependencies. - Action Types Should Be Descriptive: Use clear and descriptive action types like
add,remove, ortoggle. - Avoid Overusing Reducers: If the state logic is simple,
useStatemay be a better fit.
Common Mistakes
- Modifying State Directly: Always return a new state object in the reducer.
- Using Reducers for Simple Logic: If the state logic is straightforward,
useStateis simpler. - Overcomplicating Actions: Keep actions minimal and focused.
Best Practices
- Use constants or enums for action types to avoid typos.
- Encapsulate complex logic within the reducer for better readability.
- Structure the state and actions in a scalable way for larger components or apps.
The useReducer hook is an essential tool in a React developer’s toolkit, especially for building scalable and maintainable state logic in functional components.