1. Introduction
In React applications, state management can become challenging when complex or multi-step updates are required. Using multiple useState hooks or deeply nested objects sometimes leads to scattered logic. useReducer addresses this by providing a predictable, reducer-driven approach—similar to Redux but localized to a component.
Key Benefits:
- Predictable: All state transitions flow through a single reducer function.
- Testable: Pure reducer logic is easy to test with various inputs/outputs.
- Maintainable: Clear separation of “what happened” (actions) and “how to update” (reducer logic).
2. Basic Signature
The basic signature for useReducer is:
const [state, dispatch] = useReducer(reducer, initialArg, init?);
- reducer: A function (state, action) => newState describing how to transform current state based on an action.
- initialArg: The initial state value, or an argument passed to an optional initializer function.
- init? (Optional): A function for lazy initialization if you need to derive the starting state from initialArg.
Return Value:
- state: The current, updated state after reducer actions.
- dispatch: A function to send actions (e.g. dispatch({ type: "INCREMENT" })).
3. When to Use useReducer
- Complex or Nested State: If multiple slices of state or objects get unwieldy in useState, a reducer can organize updates cleanly.
- Multiple Transition Types: When you have distinct ways of updating state (e.g., INCREMENT, DECREMENT, RESET), a reducer pattern centralizes them.
- Predictable State Flow: If you’re used to Redux or other state machines, useReducer follows a similar pattern of dispatching actions.
- Better Testing: You can unit-test your reducer function independently from React, ensuring correctness for each action type.
4. Basic Example: Counter
A classic scenario is incrementing and decrementing a counter:
import React, { useReducer } from "react";
function counterReducer(state, action) {
switch (action.type) {
case "INCREMENT":
return state + 1;
case "DECREMENT":
return state - 1;
case "RESET":
return 0;
default:
return state;
}
}
export default function Counter() {
const [count, dispatch] = useReducer(counterReducer, 0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => dispatch({ type: "INCREMENT" })}>Increment</button>
<button onClick={() => dispatch({ type: "DECREMENT" })}>Decrement</button>
<button onClick={() => dispatch({ type: "RESET" })}>Reset</button>
</div>
);
}
Explanation:
- useReducer(counterReducer, 0) initializes state to 0.
- dispatch({ type: "INCREMENT" }) triggers the reducer logic, returning the new state.
- Each button neatly specifies an action type, making updates straightforward.
5. More Advanced Example: Form Management
Consider a multi-field form with validations:
import React, { useReducer } from "react";
const initialForm = {
username: "",
email: "",
age: "",
errors: {},
};
function formReducer(state, action) {
switch (action.type) {
case "UPDATE_FIELD": {
const { field, value } = action.payload;
return {
...state,
[field]: value,
};
}
case "SET_ERRORS": {
return {
...state,
errors: action.payload,
};
}
case "RESET_FORM":
return initialForm;
default:
return state;
}
}
export default function UserForm() {
const [formState, dispatch] = useReducer(formReducer, initialForm);
function handleChange(e) {
dispatch({
type: "UPDATE_FIELD",
payload: { field: e.target.name, value: e.target.value },
});
}
function handleSubmit(e) {
e.preventDefault();
const errors = {};
if (!formState.username) errors.username = "Username is required.";
if (!formState.email.includes("@")) errors.email = "Email is invalid.";
if (Object.keys(errors).length) {
dispatch({ type: "SET_ERRORS", payload: errors });
return;
}
// If no errors, submit or handle the data
console.log("Form Submitted:", formState);
dispatch({ type: "RESET_FORM" });
}
return (
<form onSubmit={handleSubmit}>
<label>
Username:
<input
name="username"
value={formState.username}
onChange={handleChange}
/>
{formState.errors.username && <p>{formState.errors.username}</p>}
</label>
<label>
Email:
<input
name="email"
value={formState.email}
onChange={handleChange}
/>
{formState.errors.email && <p>{formState.errors.email}</p>}
</label>
<label>
Age:
<input
name="age"
value={formState.age}
onChange={handleChange}
/>
</label>
<button type="submit">Submit</button>
<button type="button" onClick={() => dispatch({ type: "RESET_FORM" })}>
Reset
</button>
</form>
);
}
Highlights:
- formReducer handles all form-related actions, like field updates or error setting.
- Action Types: UPDATE_FIELD, SET_ERRORS, RESET_FORM.
- This structure keeps the form logic centralized and easier to maintain or test.
6. Best Practices & Tips
- Keep Reducers Pure
- No API calls or side effects in the reducer. If you need async logic, handle it outside and dispatch results.
- Meaningful Action Types
- Use descriptive strings (e.g., SUBMIT_FORM, FETCH_SUCCESS) for clarity and future-proofing.
- Consider Constants
- For large apps, define action type constants to avoid typos and keep them organized.
- useState vs. useReducer
- If your state changes are minimal (only one or two fields), useState is usually simpler.
- Move to useReducer when transitions multiply or you need a structured approach.
- Combine with useContext
- If multiple components need access to this reducer-driven state, wrap it in a context provider.
7. Potential Pitfalls
- Overkill for Simple Cases
- Don’t force useReducer if a single useState suffices. Always weigh complexity vs. benefits.
- State Mutation
- Never mutate the state directly in the reducer—always return a new object or array.
- Action Explosion
- If you have too many action types, consider splitting your reducer or reorganizing logic.
- Large Data Structures
- Cloning big objects or arrays in the reducer can get expensive. If performance becomes an issue, explore ways to optimize updates.
8. Conclusion
useReducer in React 19 offers a predictable, testable, and organized way to manage state transitions. By centralizing update logic in a single reducer function, you clearly separate what happened (the action) from how it updates the state (the reducer).
Key Takeaways
- Basic Signature:
const [state, dispatch] = useReducer(reducer, initialArg, init?);
- Predictable Updates: Each dispatched action flows through the same logic.
- Clean Separation: Keep side effects out of reducers—use them only for state transformations.
- Great for Complex State: If your app’s state transitions grow in number or sophistication, useReducer can keep things manageable.
With a solid grasp of useReducer, you’ll be better equipped to handle complex forms, multi-step wizards, and advanced UI flows in your React 19 apps—without losing clarity or maintainability.
'Dev > React' 카테고리의 다른 글
| A Practical Guide to useState in React 19 (0) | 2025.03.09 |
|---|---|
| A Practical Guide to useRef in React 19 (0) | 2025.03.09 |
| A Practical Guide to useOptimistic in React 19 (0) | 2025.03.09 |
| A Practical Guide to useMemo in React 19 (0) | 2025.03.08 |
| A Practical Guide to useLayoutEffect in React 19 (0) | 2025.03.08 |