Skip
Arish's avatar

18. useReducer Hook


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

useStateuseReducer
Simple stateComplex state logic
Few state updatesMany state updates
Independent state valuesRelated state values
No strict patternPredictable 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!