React useReducer
useReducer 是一個用於管理複雜狀態邏輯的 Hook。它是 useState 的替代方案,特別適合用於狀態邏輯複雜或狀態之間有依賴關係的情況。
基本語法
import { useReducer } from 'react'
const [state, dispatch] = useReducer(reducer, initialState)
reducer:一個函式,接收當前 state 和 action,返回新的 stateinitialState:初始狀態值state:當前狀態dispatch:用來發送 action 的函式
Reducer 函式
Reducer 是一個純函式,根據 action 類型來決定如何更新狀態:
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 }
case 'DECREMENT':
return { count: state.count - 1 }
case 'RESET':
return { count: 0 }
default:
return state
}
}
基本範例:計數器
import { useReducer } from 'react'
// 定義 reducer
function counterReducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 }
case 'DECREMENT':
return { count: state.count - 1 }
case 'RESET':
return { count: 0 }
case 'SET':
return { count: action.payload }
default:
throw new Error(`未知的 action: ${action.type}`)
}
}
// 初始狀態
const initialState = { count: 0 }
function Counter() {
const [state, dispatch] = useReducer(counterReducer, initialState)
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+1</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-1</button>
<button onClick={() => dispatch({ type: 'RESET' })}>重置</button>
<button onClick={() => dispatch({ type: 'SET', payload: 100 })}>設為 100</button>
</div>
)
}
useReducer vs useState
| 情況 | 推薦使用 |
|---|---|
| 簡單的獨立狀態 | useState |
| 多個相關的狀態值 | useReducer |
| 狀態更新邏輯複雜 | useReducer |
| 下一個狀態依賴前一個狀態 | useReducer |
| 需要在深層元件觸發更新 | useReducer + Context |
使用 useState
// 適合簡單狀態
const [name, setName] = useState('')
const [age, setAge] = useState(0)
const [email, setEmail] = useState('')
使用 useReducer
// 適合複雜狀態
const initialState = {
name: '',
age: 0,
email: '',
isSubmitting: false,
error: null,
}
function formReducer(state, action) {
switch (action.type) {
case 'UPDATE_FIELD':
return { ...state, [action.field]: action.value }
case 'SUBMIT_START':
return { ...state, isSubmitting: true, error: null }
case 'SUBMIT_SUCCESS':
return { ...state, isSubmitting: false }
case 'SUBMIT_ERROR':
return { ...state, isSubmitting: false, error: action.error }
case 'RESET':
return initialState
default:
return state
}
}
實際範例:待辦清單
import { useReducer, useState } from 'react'
// 定義 action types(可選,但有助於避免錯字)
const ACTIONS = {
ADD: 'ADD',
TOGGLE: 'TOGGLE',
DELETE: 'DELETE',
EDIT: 'EDIT',
CLEAR_COMPLETED: 'CLEAR_COMPLETED',
}
// Reducer 函式
function todoReducer(todos, action) {
switch (action.type) {
case ACTIONS.ADD:
return [
...todos,
{
id: crypto.randomUUID(),
text: action.payload,
completed: false,
},
]
case ACTIONS.TOGGLE:
return todos.map((todo) =>
todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo
)
case ACTIONS.DELETE:
return todos.filter((todo) => todo.id !== action.payload)
case ACTIONS.EDIT:
return todos.map((todo) =>
todo.id === action.payload.id ? { ...todo, text: action.payload.text } : todo
)
case ACTIONS.CLEAR_COMPLETED:
return todos.filter((todo) => !todo.completed)
default:
return todos
}
}
function TodoApp() {
const [todos, dispatch] = useReducer(todoReducer, [])
const [inputValue, setInputValue] = useState('')
function handleSubmit(e) {
e.preventDefault()
if (inputValue.trim()) {
dispatch({ type: ACTIONS.ADD, payload: inputValue.trim() })
setInputValue('')
}
}
const completedCount = todos.filter((t) => t.completed).length
const totalCount = todos.length
return (
<div>
<h1>待辦清單</h1>
<form onSubmit={handleSubmit}>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="新增待辦事項"
/>
<button type="submit">新增</button>
</form>
<ul>
{todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} dispatch={dispatch} />
))}
</ul>
{totalCount > 0 && (
<div>
<p>
完成 {completedCount} / {totalCount}
</p>
{completedCount > 0 && (
<button onClick={() => dispatch({ type: ACTIONS.CLEAR_COMPLETED })}>清除已完成</button>
)}
</div>
)}
</div>
)
}
function TodoItem({ todo, dispatch }) {
const [isEditing, setIsEditing] = useState(false)
const [editText, setEditText] = useState(todo.text)
function handleSave() {
if (editText.trim()) {
dispatch({
type: ACTIONS.EDIT,
payload: { id: todo.id, text: editText.trim() },
})
setIsEditing(false)
}
}
return (
<li>
{isEditing ? (
<>
<input value={editText} onChange={(e) => setEditText(e.target.value)} />
<button onClick={handleSave}>儲存</button>
<button onClick={() => setIsEditing(false)}>取消</button>
</>
) : (
<>
<input
type="checkbox"
checked={todo.completed}
onChange={() => dispatch({ type: ACTIONS.TOGGLE, payload: todo.id })}
/>
<span
style={{
textDecoration: todo.completed ? 'line-through' : 'none',
}}
>
{todo.text}
</span>
<button onClick={() => setIsEditing(true)}>編輯</button>
<button onClick={() => dispatch({ type: ACTIONS.DELETE, payload: todo.id })}>刪除</button>
</>
)}
</li>
)
}
搭配 Context 使用
useReducer 搭配 useContext 是一個常見的模式,可以實現類似 Redux 的全域狀態管理:
import { createContext, useContext, useReducer } from 'react'
// 建立 Context
const TodoContext = createContext(null)
// Reducer
function todoReducer(state, action) {
switch (action.type) {
case 'ADD':
return {
...state,
todos: [
...state.todos,
{
id: crypto.randomUUID(),
text: action.payload,
completed: false,
},
],
}
case 'TOGGLE':
return {
...state,
todos: state.todos.map((todo) =>
todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo
),
}
case 'SET_FILTER':
return { ...state, filter: action.payload }
default:
return state
}
}
const initialState = {
todos: [],
filter: 'all', // 'all', 'active', 'completed'
}
// Provider 元件
function TodoProvider({ children }) {
const [state, dispatch] = useReducer(todoReducer, initialState)
return <TodoContext value={{ state, dispatch }}>{children}</TodoContext>
}
// 自訂 Hook
function useTodos() {
const context = useContext(TodoContext)
if (!context) {
throw new Error('useTodos 必須在 TodoProvider 內使用')
}
return context
}
// 使用範例
function TodoList() {
const { state, dispatch } = useTodos()
const filteredTodos = state.todos.filter((todo) => {
if (state.filter === 'active') return !todo.completed
if (state.filter === 'completed') return todo.completed
return true
})
return (
<ul>
{filteredTodos.map((todo) => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => dispatch({ type: 'TOGGLE', payload: todo.id })}
/>
{todo.text}
</li>
))}
</ul>
)
}
function FilterButtons() {
const { state, dispatch } = useTodos()
return (
<div>
{['all', 'active', 'completed'].map((filter) => (
<button
key={filter}
onClick={() => dispatch({ type: 'SET_FILTER', payload: filter })}
style={{ fontWeight: state.filter === filter ? 'bold' : 'normal' }}
>
{filter}
</button>
))}
</div>
)
}
Lazy Initialization
如果初始狀態需要複雜計算,可以使用第三個參數來延遲初始化:
function init(initialCount) {
return { count: initialCount }
}
function Counter({ initialCount }) {
const [state, dispatch] = useReducer(reducer, initialCount, init)
// ...
}
這樣 init 函式只會在元件首次渲染時執行。