useReducer Hook
useReducer is an alternative to useState for complex state logic. It's like having a mini Redux inside your component.
Basic Syntax
jsx
1import { useReducer } from 'react'
2
3function reducer(state, action) {
4 switch (action.type) {
5 case 'increment':
6 return { count: state.count + 1 }
7 case 'decrement':
8 return { count: state.count - 1 }
9 default:
10 return state
11 }
12}
13
14function Counter() {
15 const [state, dispatch] = useReducer(reducer, { count: 0 })
16
17 return (
18 <div>
19 <p>Count: {state.count}</p>
20 <button onClick={() => dispatch({ type: 'increment' })}>+</button>
21 <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
22 </div>
23 )
24}useState vs useReducer
| useState | useReducer |
|---|---|
| Simple state | Complex state logic |
| Few state updates | Many state updates |
| Independent state values | Related state values |
| No strict pattern | Predictable reducer pattern |
jsx
1// useState - good for simple state
2const [count, setCount] = useState(0)
3const [name, setName] = useState('')
4
5// useReducer - good for complex state
6const [state, dispatch] = useReducer(reducer, {
7 count: 0,
8 step: 1,
9 history: []
10})Reducer Pattern
jsx
1// Action types (optional but recommended)
2const ACTIONS = {
3 INCREMENT: 'INCREMENT',
4 DECREMENT: 'DECREMENT',
5 SET_STEP: 'SET_STEP',
6 RESET: 'RESET'
7}
8
9// Initial state
10const initialState = {
11 count: 0,
12 step: 1
13}
14
15// Reducer function - must be pure!
16function counterReducer(state, action) {
17 switch (action.type) {
18 case ACTIONS.INCREMENT:
19 return { ...state, count: state.count + state.step }
20
21 case ACTIONS.DECREMENT:
22 return { ...state, count: state.count - state.step }
23
24 case ACTIONS.SET_STEP:
25 return { ...state, step: action.payload }
26
27 case ACTIONS.RESET:
28 return initialState
29
30 default:
31 throw new Error(`Unknown action: ${action.type}`)
32 }
33}
34
35function Counter() {
36 const [state, dispatch] = useReducer(counterReducer, initialState)
37
38 return (
39 <div>
40 <p>Count: {state.count}</p>
41 <p>Step: {state.step}</p>
42
43 <button onClick={() => dispatch({ type: ACTIONS.INCREMENT })}>
44 +{state.step}
45 </button>
46 <button onClick={() => dispatch({ type: ACTIONS.DECREMENT })}>
47 -{state.step}
48 </button>
49
50 <input
51 type="number"
52 value={state.step}
53 onChange={(e) => dispatch({
54 type: ACTIONS.SET_STEP,
55 payload: parseInt(e.target.value) || 1
56 })}
57 />
58
59 <button onClick={() => dispatch({ type: ACTIONS.RESET })}>
60 Reset
61 </button>
62 </div>
63 )
64}Todo App with useReducer
jsx
1const ACTIONS = {
2 ADD_TODO: 'ADD_TODO',
3 TOGGLE_TODO: 'TOGGLE_TODO',
4 DELETE_TODO: 'DELETE_TODO',
5 EDIT_TODO: 'EDIT_TODO',
6 CLEAR_COMPLETED: 'CLEAR_COMPLETED',
7 SET_FILTER: 'SET_FILTER'
8}
9
10const initialState = {
11 todos: [],
12 filter: 'all' // 'all', 'active', 'completed'
13}
14
15function todoReducer(state, action) {
16 switch (action.type) {
17 case ACTIONS.ADD_TODO:
18 return {
19 ...state,
20 todos: [
21 ...state.todos,
22 {
23 id: Date.now(),
24 text: action.payload,
25 completed: false
26 }
27 ]
28 }
29
30 case ACTIONS.TOGGLE_TODO:
31 return {
32 ...state,
33 todos: state.todos.map(todo =>
34 todo.id === action.payload
35 ? { ...todo, completed: !todo.completed }
36 : todo
37 )
38 }
39
40 case ACTIONS.DELETE_TODO:
41 return {
42 ...state,
43 todos: state.todos.filter(todo => todo.id !== action.payload)
44 }
45
46 case ACTIONS.EDIT_TODO:
47 return {
48 ...state,
49 todos: state.todos.map(todo =>
50 todo.id === action.payload.id
51 ? { ...todo, text: action.payload.text }
52 : todo
53 )
54 }
55
56 case ACTIONS.CLEAR_COMPLETED:
57 return {
58 ...state,
59 todos: state.todos.filter(todo => !todo.completed)
60 }
61
62 case ACTIONS.SET_FILTER:
63 return {
64 ...state,
65 filter: action.payload
66 }
67
68 default:
69 return state
70 }
71}
72
73function TodoApp() {
74 const [state, dispatch] = useReducer(todoReducer, initialState)
75 const [input, setInput] = useState('')
76
77 const filteredTodos = state.todos.filter(todo => {
78 if (state.filter === 'active') return !todo.completed
79 if (state.filter === 'completed') return todo.completed
80 return true
81 })
82
83 const handleSubmit = (e) => {
84 e.preventDefault()
85 if (input.trim()) {
86 dispatch({ type: ACTIONS.ADD_TODO, payload: input })
87 setInput('')
88 }
89 }
90
91 return (
92 <div>
93 <form onSubmit={handleSubmit}>
94 <input
95 value={input}
96 onChange={(e) => setInput(e.target.value)}
97 placeholder="Add todo..."
98 />
99 <button type="submit">Add</button>
100 </form>
101
102 <div>
103 {['all', 'active', 'completed'].map(filter => (
104 <button
105 key={filter}
106 onClick={() => dispatch({ type: ACTIONS.SET_FILTER, payload: filter })}
107 className={state.filter === filter ? 'active' : ''}
108 >
109 {filter}
110 </button>
111 ))}
112 </div>
113
114 <ul>
115 {filteredTodos.map(todo => (
116 <li key={todo.id}>
117 <input
118 type="checkbox"
119 checked={todo.completed}
120 onChange={() => dispatch({ type: ACTIONS.TOGGLE_TODO, payload: todo.id })}
121 />
122 <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
123 {todo.text}
124 </span>
125 <button onClick={() => dispatch({ type: ACTIONS.DELETE_TODO, payload: todo.id })}>
126 Delete
127 </button>
128 </li>
129 ))}
130 </ul>
131
132 <button onClick={() => dispatch({ type: ACTIONS.CLEAR_COMPLETED })}>
133 Clear Completed
134 </button>
135 </div>
136 )
137}Lazy Initialization
For expensive initial state:
jsx
1function init(initialCount) {
2 // Expensive computation or localStorage read
3 return { count: initialCount }
4}
5
6function Counter({ initialCount }) {
7 const [state, dispatch] = useReducer(reducer, initialCount, init)
8 // init function only runs once
9}Action Creators
Helper functions to create actions:
jsx
1// Action creators
2const addTodo = (text) => ({ type: ACTIONS.ADD_TODO, payload: text })
3const toggleTodo = (id) => ({ type: ACTIONS.TOGGLE_TODO, payload: id })
4const deleteTodo = (id) => ({ type: ACTIONS.DELETE_TODO, payload: id })
5
6// Usage
7dispatch(addTodo('Learn React'))
8dispatch(toggleTodo(123))
9dispatch(deleteTodo(123))Combining with Context
jsx
1const TodoContext = createContext()
2
3function TodoProvider({ children }) {
4 const [state, dispatch] = useReducer(todoReducer, initialState)
5
6 return (
7 <TodoContext.Provider value={{ state, dispatch }}>
8 {children}
9 </TodoContext.Provider>
10 )
11}
12
13function useTodos() {
14 const context = useContext(TodoContext)
15 if (!context) {
16 throw new Error('useTodos must be used within TodoProvider')
17 }
18 return context
19}useReducer brings predictable state management to React!
