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 currentstate
and anaction
, and returns the updated state.initialState
: The initial state value.state
: The current state managed by theuseReducer
hook.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:
useReducer
centralizes all state logic in the reducer function, making it easier to manage complex states. - Action-Based Updates: State changes are triggered by
dispatch
ing an action. Actions have atype
and 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
useReducer
overuseState
when 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,
useState
may 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,
useState
is 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.