- Animation
- Bootstrap
- Button
- Components
- Conditional rendering
- Context API
- Debounce
- Error boundary
- Events
- Form
- Fragment
- Hooks
- Inline styling
- Key
- Lazy loading
- Lifecycle methods
- Portal
- Prop types
- Props
- Redux library
- Ref
- Router
- State
- State management
- Strict mode
- Suspense
- useCallback Hook
- useEffect hook
- useMemo hook
- useReducer hook
- useRef hook
- Virtual DOM
REACT
React useReducer Hook: Syntax, Usage, and Examples
The React useReducer hook helps you manage complex state logic in function components. It’s especially useful when state changes depend on previous values or involve multiple sub-values. This hook works similarly to a Redux reducer: you dispatch actions, and the reducer function determines how state updates.
You might choose useReducer React patterns over useState when dealing with more structured or scalable application state.
How React useReducer Works
The useReducer hook accepts two arguments: a reducer function and an initial state. It returns the current state and a dispatch function to trigger changes.
Syntax
const [state, dispatch] = useReducer(reducer, initialState);
The reducer
is a pure function that receives the current state and an action, then returns a new state.
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return state;
}
}
To update the state, you call the dispatch
function with an action object:
dispatch({ type: 'increment' });
Example: useReducer for a Counter
Here's a simple counter example using the React usereducer hook:
import { useReducer } from 'react';
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<><p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
</>
);
}
This approach separates the state logic from the UI, making the component easier to test and maintain.
You Can Manage More Complex State
useReducer shines when your state has multiple values or you need to update it based on actions.
const initialState = {
user: null,
loading: false,
error: null
};
function reducer(state, action) {
switch (action.type) {
case 'FETCH_START':
return { ...state, loading: true };
case 'FETCH_SUCCESS':
return { ...state, loading: false, user: action.payload };
case 'FETCH_ERROR':
return { ...state, loading: false, error: action.payload };
default:
return state;
}
}
In this case, a single reducer handles all logic related to fetching a user.
You Should Use useReducer When State Logic Grows
React useState works well for simple use cases. But when you:
- Depend on previous state for updates
- Have multiple values in a single state object
- Want better organization
- Need a Redux-like pattern without Redux
…you should consider using useReducer.
This is often the case in form management, nested updates, undo/redo functionality, and state machines.
useReducer with Initial State from Props
Sometimes you want to derive initial state from props. React lets you use an initializer function as a third argument to useReducer.
const init = (initialCount) => ({ count: initialCount });
const [state, dispatch] = useReducer(reducer, 5, init);
This pattern lets you defer expensive setup logic until the component actually mounts.
You Can Combine useReducer with Context
For global state sharing, combine useReducer React logic with React Context. This mimics Redux without extra libraries.
const StateContext = createContext();
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<StateContext.Provider value={{ state, dispatch }}>
<ChildComponent />
</StateContext.Provider>
);
}
function ChildComponent() {
const { state, dispatch } = useContext(StateContext);
return <div>{state.value}</div>;
}
This pattern improves scalability and readability in large applications.
How to Use useReducer in React for Controlled Forms
Managing form state with useReducer lets you centralize logic and validations.
const formReducer = (state, action) => {
return { ...state, [action.name]: action.value };
};
const [formState, dispatch] = useReducer(formReducer, {
username: "",
email: ""
});
<inputname="username"
value={formState.username}
onChange={(e) =>
dispatch({ name: e.target.name, value: e.target.value })
}
/>
This approach avoids managing multiple useState hooks for each field.
Debugging and Testing Reducers
Because reducer functions are pure, you can write tests for them without rendering components.
test('should increment count', () => {
const initialState = { count: 0 };
const action = { type: 'increment' };
const result = reducer(initialState, action);
expect(result.count).toBe(1);
});
This makes useReducer a great fit for apps that need strong test coverage.
Best Practices for useReducer in React
- Keep the reducer pure: no side effects or async code inside it.
- Use constants for action types to avoid typos.
- Create action creators for readability.
- Separate logic-heavy reducers into their own files.
- Avoid unnecessary nesting of state.
Following these practices ensures that your useReducer logic is maintainable and scalable.
useReducer vs useState
Choosing between useReducer
and useState
depends on the complexity of the state you're managing.
- Use
useState
when you're working with simple, independent values like toggles, counters, or form fields with straightforward logic. It's lightweight and easy to understand for these cases. - Switch to
useReducer
when state updates depend on previous values, when you have multiple related pieces of state, or when the logic for updating state gets repetitive or hard to manage with multipleuseState
hooks. - If your component needs to handle more structured logic, like conditionally updating nested properties or running multiple state updates at once,
useReducer
gives you a clearer and more maintainable way to do it. - In large applications or shared state scenarios, combining
useReducer
withContext
can provide a Redux-like structure without the extra libraries.
By thinking about the shape and complexity of your state, you can choose the tool that fits best—useState
for simplicity, or useReducer
for structure and control.
Example: useReducer in a Todo App
const initialState = [];
function todoReducer(state, action) {
switch (action.type) {
case "ADD_TODO":
return [...state, { id: Date.now(), text: action.payload }];
case "REMOVE_TODO":
return state.filter(todo => todo.id !== action.payload);
default:
return state;
}
}
Use dispatch to add or remove todos:
dispatch({ type: "ADD_TODO", payload: "Buy milk" });
dispatch({ type: "REMOVE_TODO", payload: 12345 });
This structure handles dynamic lists in a clean, scalable way.
The useReducer React hook offers a structured way to manage state updates, especially when those updates rely on previous values or involve multiple conditions. You define a reducer function, pass an initial state, and use dispatch to trigger changes.
By understanding how to use useReducer in React effectively, you can:
- Simplify complex logic
- Keep state transitions predictable
- Centralize updates into one function
- Improve testability and organization
This hook is a solid choice for advanced state management in React apps without needing a full external state library.
Sign up or download Mimo from the App Store or Google Play to enhance your programming skills and prepare for a career in tech.