Redux 狀態管理
Redux Toolkit (RTK) 完整實戰:Store 設計、Slice、非同步 Thunk、RTK Query 與 Selector 最佳化。
核心概念
Store
整個應用的單一資料來源。由 configureStore 建立,接收多個 reducer 組合成完整的 state 樹。
Slice
createSlice 將一個功能模組的 initialState、reducers、action creators 整合在一起,大幅減少樣板程式碼。
Immer(自動 Immutable)
RTK 內建 Immer,reducer 中可以直接 mutate state(如 state.value += 1),Immer 會在底層產生新的 immutable 物件,不需手動展開。
Dispatch
呼叫 dispatch(action) 將 action 送進 Store,Store 呼叫 reducer 計算新 state 並通知所有訂閱者(元件)重新 render。
Selector
透過 useSelector 從 Store 讀取資料。搭配 createSelector 可做 memoized 衍生計算,避免不必要的重新 render。
Middleware
位於 dispatch → reducer 之間的擴充層。RTK 預設加入 redux-thunk(處理非同步) 和 serializability check。可透過 getDefaultMiddleware 追加自訂 middleware。
完整初始設定(TypeScript)
RTK 與 TypeScript 整合的標準檔案結構,匯出型別化的 hooks 供全專案使用。
store.ts — 建立 Store 並匯出型別
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './features/counter/counterSlice'
import userReducer from './features/user/userSlice'
export const store = configureStore({
reducer: {
counter: counterReducer,
user: userReducer,
},
})
// 從 store 推導出全域 RootState 型別,不需手動維護
export type RootState = ReturnType<typeof store.getState>
// AppDispatch 保留 thunk 型別資訊
export type AppDispatch = typeof store.dispatchhooks.ts — 型別化的 useAppSelector / useAppDispatch
專案中永遠使用這兩個 hook,而非直接用 useSelector / useDispatch,確保型別安全。
import { useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './store'
// 使用 useAppDispatch 才能讓 dispatch(thunk) 有正確的型別
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()Provider — 掛載到 React 根元件
// app/layout.tsx 或 _app.tsx
import { Provider } from 'react-redux'
import { store } from '@/lib/store'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<Provider store={store}>{children}</Provider>
</body>
</html>
)
}createSlice 深入
initialState 型別設計 + 完整 Slice
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
interface Todo {
id: string
text: string
completed: boolean
}
interface TodoState {
items: Todo[]
filter: 'all' | 'active' | 'completed'
}
const initialState: TodoState = {
items: [],
filter: 'all',
}
const todoSlice = createSlice({
name: 'todos',
initialState,
reducers: {
addTodo: {
// prepare callback:在存入 state 前先加工 payload
reducer(state, action: PayloadAction<Todo>) {
state.items.push(action.payload)
},
prepare(text: string) {
return {
payload: { id: crypto.randomUUID(), text, completed: false },
}
},
},
toggleTodo(state, action: PayloadAction<string>) {
const todo = state.items.find((t) => t.id === action.payload)
if (todo) todo.completed = !todo.completed // Immer 允許直接 mutate
},
removeTodo(state, action: PayloadAction<string>) {
state.items = state.items.filter((t) => t.id !== action.payload)
},
setFilter(state, action: PayloadAction<TodoState['filter']>) {
state.filter = action.payload
},
// 重置整個 slice 狀態
resetTodos: () => initialState,
},
})
export const { addTodo, toggleTodo, removeTodo, setFilter, resetTodos } = todoSlice.actions
export default todoSlice.reducerreducers
定義同步 action。每個 key 自動產生對應的 action creator,呼叫時傳入的參數即為 action.payload。
extraReducers
回應其他 slice 或 thunk 產生的 action,通常用來處理 createAsyncThunk 的三個狀態。
prepare callback
在 payload 存入 reducer 前對其加工,例如產生 id、加上 timestamp 等,讓 reducer 保持 pure function。
重置 reset 技巧
resetTodos: () => initialState:直接回傳 initialState 替換掉整個 slice state,不需展開。
createAsyncThunk — 非同步資料流
RTK 內建的非同步 action 工具,自動產生 pending / fulfilled / rejected 三個 action,並與 extraReducers 搭配處理 loading / error / data 狀態。
1. 定義 Thunk + 完整 Slice(含 loading state)
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
interface User {
id: number
name: string
email: string
}
interface UserState {
data: User | null
status: 'idle' | 'loading' | 'succeeded' | 'failed'
error: string | null
}
const initialState: UserState = { data: null, status: 'idle', error: null }
// createAsyncThunk<回傳型別, 參數型別>
export const fetchUser = createAsyncThunk<User, number>(
'user/fetchById',
async (userId, { rejectWithValue }) => {
try {
const res = await fetch(`/api/users/${userId}`)
if (!res.ok) throw new Error('Server error')
return (await res.json()) as User
} catch (err) {
// rejectWithValue 讓 rejected.payload 有型別(非 Error 物件)
return rejectWithValue((err as Error).message)
}
}
)
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
clearUser: (state) => { state.data = null; state.status = 'idle' },
},
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => {
state.status = 'loading'
state.error = null
})
.addCase(fetchUser.fulfilled, (state, action) => {
state.status = 'succeeded'
state.data = action.payload // 型別安全:User
})
.addCase(fetchUser.rejected, (state, action) => {
state.status = 'failed'
state.error = action.payload as string ?? 'Unknown error'
})
},
})
export const { clearUser } = userSlice.actions
export default userSlice.reducer2. 在元件中 dispatch Thunk + 處理 UI 狀態
'use client'
import { useEffect } from 'react'
import { useAppDispatch, useAppSelector } from '@/lib/hooks'
import { fetchUser } from '@/lib/features/user/userSlice'
export default function UserProfile({ userId }: { userId: number }) {
const dispatch = useAppDispatch()
const { data, status, error } = useAppSelector((state) => state.user)
useEffect(() => {
dispatch(fetchUser(userId))
}, [dispatch, userId])
if (status === 'loading') return <p>Loading...</p>
if (status === 'failed') return <p className="text-red-500">{error}</p>
if (!data) return null
return (
<div>
<h2>{data.name}</h2>
<p>{data.email}</p>
</div>
)
}取消 Thunk(AbortController)
dispatch(thunk) 回傳一個帶有 .abort() 方法的 Promise,搭配 signal 參數可在元件 unmount 時取消進行中的請求,防止 race condition。
// thunk 內部
async (userId, { signal }) => {
const res = await fetch(`/api/users/${userId}`, { signal })
...
}
// 元件中
useEffect(() => {
const promise = dispatch(fetchUser(userId))
return () => promise.abort() // unmount 時取消
}, [dispatch, userId])RTK Query — 資料請求與快取
RTK Query 是 RTK 內建的強力資料請求解決方案,自動處理 loading / caching / refetching / invalidation,大幅減少手寫 thunk + extraReducers 的樣板。
1. 定義 API Slice(apiSlice.ts)
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
interface Post { id: number; title: string; body: string }
export const postsApi = createApi({
reducerPath: 'postsApi', // store 裡的 key
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['Post'], // 用於 cache invalidation
endpoints: (builder) => ({
// query:GET 請求
getPosts: builder.query<Post[], void>({
query: () => '/posts',
providesTags: ['Post'],
}),
getPostById: builder.query<Post, number>({
query: (id) => `/posts/${id}`,
providesTags: (result, error, id) => [{ type: 'Post', id }],
}),
// mutation:POST / PUT / DELETE
createPost: builder.mutation<Post, Partial<Post>>({
query: (body) => ({ url: '/posts', method: 'POST', body }),
// 建立成功後,使所有 'Post' 快取失效 → 自動 refetch
invalidatesTags: ['Post'],
}),
deletePost: builder.mutation<void, number>({
query: (id) => ({ url: `/posts/${id}`, method: 'DELETE' }),
invalidatesTags: (result, error, id) => [{ type: 'Post', id }],
}),
}),
})
// 自動產生的 hooks
export const {
useGetPostsQuery,
useGetPostByIdQuery,
useCreatePostMutation,
useDeletePostMutation,
} = postsApi2. 將 RTK Query reducer 加入 Store
import { configureStore } from '@reduxjs/toolkit'
import { postsApi } from './features/posts/apiSlice'
export const store = configureStore({
reducer: {
[postsApi.reducerPath]: postsApi.reducer, // RTK Query 管理的快取
// ...其他 slice reducer
},
// 加入 RTK Query middleware(負責快取生命週期管理)
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(postsApi.middleware),
})3. 在元件中使用(自動快取)
'use client'
import {
useGetPostsQuery,
useCreatePostMutation,
useDeletePostMutation,
} from '@/lib/features/posts/apiSlice'
export default function PostList() {
// 自動 fetch、快取、loading/error 狀態一次搞定
const { data: posts, isLoading, isError, refetch } = useGetPostsQuery()
const [createPost, { isLoading: isCreating }] = useCreatePostMutation()
const [deletePost] = useDeletePostMutation()
if (isLoading) return <p>Loading...</p>
if (isError) return <button onClick={refetch}>重試</button>
return (
<ul>
{posts?.map((post) => (
<li key={post.id}>
{post.title}
<button onClick={() => deletePost(post.id)}>刪除</button>
</li>
))}
<button
disabled={isCreating}
onClick={() => createPost({ title: 'New Post', body: '...' })}
>
新增
</button>
</ul>
)
}providesTags / invalidatesTags
Cache invalidation 機制。mutation 執行後帶有 invalidatesTags,所有 query 中標記了相符 providesTags 的快取會被清除並自動重新請求。
Polling(輪詢)
useGetPostsQuery(undefined, { pollingInterval: 30000 }):每 30 秒自動 refetch,適合需要即時更新的場景,無需手動 setInterval。
Skip 與條件式請求
useGetPostByIdQuery(id, { skip: !id }):當 id 為 falsy 時跳過請求,避免多餘 API 呼叫。
vs createAsyncThunk
RTK Query 適合標準 CRUD API(自動快取、去重複請求);createAsyncThunk 適合複雜非同步副作用(WebSocket、批次操作、依賴其他 state 的邏輯)。
createSelector — Memoized Selector
來自 Reselect 库(RTK 已內建),透過 memoization 避免 useSelector 每次 render 都重新計算衍生資料,有效減少不必要的 re-render。
基礎用法 vs 問題場景
import { createSelector } from '@reduxjs/toolkit'
import type { RootState } from '@/lib/store'
// ❌ 每次 render 都重算,產生新陣列 → 元件必然 re-render
const badSelector = (state: RootState) =>
state.todos.items.filter((t) => t.completed)
// ✅ createSelector:只有在 state.todos.items 或 state.todos.filter 變更時才重算
const selectTodosItems = (state: RootState) => state.todos.items
const selectFilter = (state: RootState) => state.todos.filter
export const selectFilteredTodos = createSelector(
[selectTodosItems, selectFilter], // input selectors
(items, filter) => { // result function(memoized)
if (filter === 'all') return items
return items.filter((t) =>
filter === 'completed' ? t.completed : !t.completed
)
}
)
// 帶參數的 selector(工廠模式,每個元件有獨立 memoize 快取)
export const makeSelectTodoById = () =>
createSelector(
[(state: RootState) => state.todos.items, (_: RootState, id: string) => id],
(items, id) => items.find((t) => t.id === id)
)
// 在元件中
const filteredTodos = useAppSelector(selectFilteredTodos)
// 工廠模式使用
const selectTodoById = useMemo(makeSelectTodoById, [])
const todo = useAppSelector((state) => selectTodoById(state, todoId))Redux 資料流全覽
UI 互動 │ ▼ dispatch(action) / dispatch(thunk) Middleware(redux-thunk、logger 等) │ 同步 action 直接到 Reducer │ 非同步 thunk 執行副作用後再 dispatch action ▼ Reducer(createSlice reducers / extraReducers) │ pure function:(currentState, action) => newState │ Immer 讓你寫 mutating 語法,底層仍產生新物件 ▼ Store(單一 state 樹) │ state 更新後通知所有 useSelector 訂閱者 ▼ useSelector(+ createSelector memoization) │ 只有選取的資料真正變化才觸發 re-render ▼ UI 重新渲染
適合放入 Redux
- 多元件共享的全域狀態
- 伺服器資料快取(RTK Query)
- 使用者認證 / 權限資訊
- 複雜的跨元件互動狀態
不適合放入 Redux
- 本地 UI 狀態(modal 開關)
- 表單暫存輸入值
- 只有單一元件使用的資料
- 可從其他 state 衍生的資料
效能最佳化要點
- 使用 createSelector memoize 衍生資料
- Selector 顆粒度要細(避免選整個 slice)
- mutation 用 RTK Query 取代手寫 thunk
- 搭配 React.memo 減少子元件 re-render