Once you understand the basics of useReducer, you can leverage powerful patterns to handle complex scenarios.
Lazy Initialization
When initial state requires expensive computation, use the third argument:
tsxfunction init(initialCount: number): State { // Expensive computation or localStorage read const saved = localStorage.getItem('counter'); return saved ? JSON.parse(saved) : { count: initialCount, step: 1, history: [] }; } function Counter({ initialCount = 0 }) { const [state, dispatch] = useReducer(reducer, initialCount, init); // ... }
The init function only runs once during the initial render.
Immer Integration
For deeply nested state, combine with Immer for immutable updates:
tsximport { useImmerReducer } from 'use-immer'; function reducer(draft: State, action: Action) { switch (action.type) { case 'updateNestedValue': // Mutate directly - Immer handles immutability draft.user.settings.theme = action.payload; break; case 'addItem': draft.items.push(action.payload); break; } }
Action Creators
For type safety and consistency, create action creator functions:
tsxconst actions = { increment: () => ({ type: 'increment' as const }), decrement: () => ({ type: 'decrement' as const }), setStep: (step: number) => ({ type: 'setStep' as const, payload: step }), reset: () => ({ type: 'reset' as const }) }; // Usage dispatch(actions.setStep(5));
Combining Multiple Reducers
For very complex state, split into multiple reducers:
tsxfunction combineReducers<S>( reducers: { [K in keyof S]: (state: S[K], action: Action) => S[K] } ) { return (state: S, action: Action): S => { const newState = {} as S; for (const key in reducers) { newState[key] = reducers[key](state[key], action); } return newState; }; } const rootReducer = combineReducers({ user: userReducer, posts: postsReducer, ui: uiReducer });