19.1 什麼是 Redux?
Redux 是一個可預測的狀態管理工具,適合管理大型應用的全域狀態。
Redux 的三大原則
- 單一資料源:整個應用的 state 存在一個 Store 中
- State 是唯讀的:只能透過 dispatch action 來改變
- 純函式更新:Reducer 是純函式
何時需要 Redux?
| 用 Context + useReducer | 用 Redux Toolkit |
|---|---|
| 中小型應用 | 大型應用 |
| 少量全域狀態 | 大量全域狀態 |
| 簡單的更新邏輯 | 複雜的更新邏輯 |
| 不需要中介軟體 | 需要非同步處理、日誌 |
19.2 安裝 Redux Toolkit
npm install @reduxjs/toolkit react-redux
19.3 核心概念
UI → dispatch(action) → Middleware → Reducer → Store → UI 更新
建立 Slice(Redux Toolkit 的核心)
// features/counter/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0,
history: [],
},
reducers: {
increment: (state) => {
state.value += 1; // Redux Toolkit 使用 Immer,可以「直接修改」state
state.history.push(`+1 → ${state.value}`);
},
decrement: (state) => {
state.value -= 1;
state.history.push(`-1 → ${state.value}`);
},
incrementByAmount: (state, action) => {
state.value += action.payload;
state.history.push(`+${action.payload} → ${state.value}`);
},
reset: (state) => {
state.value = 0;
state.history = [];
},
},
});
// 導出 action creators
export const { increment, decrement, incrementByAmount, reset } = counterSlice.actions;
// 導出 reducer
export default counterSlice.reducer;
建立 Store
// app/store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
const store = configureStore({
reducer: {
counter: counterReducer,
},
});
export default store;
在應用中提供 Store
// main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import store from './app/store';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}>
<App />
</Provider>
);
在元件中使用
// features/counter/Counter.jsx
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, incrementByAmount, reset } from './counterSlice';
function Counter() {
const count = useSelector((state) => state.counter.value);
const history = useSelector((state) => state.counter.history);
const dispatch = useDispatch();
return (
<div style={{ maxWidth: '400px', margin: '20px auto', textAlign: 'center' }}>
<h1 style={{ fontSize: '4rem' }}>{count}</h1>
<div style={{ display: 'flex', gap: '8px', justifyContent: 'center' }}>
<button onClick={() => dispatch(decrement())}>-1</button>
<button onClick={() => dispatch(increment())}>+1</button>
<button onClick={() => dispatch(incrementByAmount(5))}>+5</button>
<button onClick={() => dispatch(reset())}>重置</button>
</div>
<div style={{ marginTop: '20px', textAlign: 'left' }}>
<h3>操作歷史</h3>
<ul>
{history.map((entry, i) => (
<li key={i}>{entry}</li>
))}
</ul>
</div>
</div>
);
}
export default Counter;
19.4 非同步操作 — createAsyncThunk
// features/posts/postsSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// 建立非同步 Thunk
export const fetchPosts = createAsyncThunk(
'posts/fetchPosts',
async (_, { rejectWithValue }) => {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=10');
if (!response.ok) throw new Error('載入失敗');
return await response.json();
} catch (error) {
return rejectWithValue(error.message);
}
}
);
export const createPost = createAsyncThunk(
'posts/createPost',
async (postData, { rejectWithValue }) => {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(postData),
});
return await response.json();
} catch (error) {
return rejectWithValue(error.message);
}
}
);
const postsSlice = createSlice({
name: 'posts',
initialState: {
items: [],
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null,
},
reducers: {
deletePost: (state, action) => {
state.items = state.items.filter((post) => post.id !== action.payload);
},
},
extraReducers: (builder) => {
builder
// fetchPosts
.addCase(fetchPosts.pending, (state) => {
state.status = 'loading';
state.error = null;
})
.addCase(fetchPosts.fulfilled, (state, action) => {
state.status = 'succeeded';
state.items = action.payload;
})
.addCase(fetchPosts.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload;
})
// createPost
.addCase(createPost.fulfilled, (state, action) => {
state.items.unshift(action.payload);
});
},
});
export const { deletePost } = postsSlice.actions;
export default postsSlice.reducer;
使用非同步 Thunk 的元件
// features/posts/PostList.jsx
import { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchPosts, deletePost } from './postsSlice';
function PostList() {
const dispatch = useDispatch();
const { items: posts, status, error } = useSelector((state) => state.posts);
useEffect(() => {
if (status === 'idle') {
dispatch(fetchPosts());
}
}, [status, dispatch]);
if (status === 'loading') return <p>載入中...</p>;
if (status === 'failed') return <p style={{ color: 'red' }}>錯誤:{error}</p>;
return (
<div>
<h2>文章列表(Redux Toolkit)</h2>
{posts.map((post) => (
<div key={post.id} style={{
padding: '12px',
borderBottom: '1px solid #eee',
display: 'flex',
justifyContent: 'space-between',
}}>
<div>
<strong>{post.title}</strong>
<p style={{ color: '#666', margin: '4px 0 0' }}>
{post.body?.substring(0, 80)}...
</p>
</div>
<button onClick={() => dispatch(deletePost(post.id))}>刪除</button>
</div>
))}
</div>
);
}
export default PostList;
19.5 多個 Slice 整合
// app/store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
import postsReducer from '../features/posts/postsSlice';
import authReducer from '../features/auth/authSlice';
const store = configureStore({
reducer: {
counter: counterReducer,
posts: postsReducer,
auth: authReducer,
},
});
export default store;
// State 結構:
// {
// counter: { value: 0, history: [] },
// posts: { items: [], status: 'idle', error: null },
// auth: { user: null, token: null, status: 'idle' },
// }
19.6 練習題
練習:建立一個完整的 Todo App with Redux Toolkit
todosSlice:管理待辦事項(CRUD)filterSlice:管理過濾條件- 非同步載入初始資料
- 樂觀更新(先更新 UI,失敗再還原)
本課重點回顧
- Redux Toolkit 大幅簡化了 Redux 的使用
createSlice自動產生 action creators 和 reducer- 可以在 reducer 中「直接修改」state(Immer 處理不可變性)
createAsyncThunk處理非同步操作useSelector讀取 state,useDispatch發送 actionextraReducers處理非同步 thunk 的三種狀態