Redux Toolkit 詳細介紹
RTK 的優點是:
-
簡化設定:自動處理很多 Redux 的 boilerplate(樣板程式碼),像 store 設定、DevTools 整合等。
-
使用 Immer:讓你可以用「可變式」(mutative)寫法來更新狀態,但實際上還是保持不可變(immutable),避免出錯。
-
內建最佳實踐:包含 Redux Thunk 來處理非同步操作,還有一個強大的 RTK Query 來管理 API 請求。
-
官方推薦:Redux 團隊強烈建議用 RTK 來寫 Redux 邏輯,無論你是新手還是老鳥,都能更快上手。
RTK 不會改變 Redux 的核心概念:狀態存放在一個單一的 store 中,透過 action 來描述變化,reducer 來更新狀態。但它讓寫程式碼變得更簡單。
1. 安裝 Redux Toolkit
首先,我們需要安裝 RTK 和 React-Redux(用來連接 React 元件和 Redux store)。假設你已經有個 React 專案(用 Create React App 建立的),在終端機(terminal)裡執行以下指令:
npm install @reduxjs/toolkit react-redux
2. 建立 Redux Store
在 RTK 中,store 的設定用 configureStore 來做,它會自動整合 Redux DevTools(瀏覽器開發工具的 Redux 擴充套件),讓你能追蹤狀態變化。
步驟:
-
在 src 資料夾下建立一個 store 資料夾。
-
在 src/store 裡建立 index.js 檔案。
完整程式碼(src/store/index.js):
import { configureStore } from "@reduxjs/toolkit";
// 之後我們會在這裡匯入 reducer,這裡先留空
// import counterReducer from '../features/counter/counterSlice';
const store = configureStore({
reducer: {
// 這裡會放所有 slice 的 reducer,例如:
// counter: counterReducer,
},
});
// 匯出 store,讓其他檔案能用
export default store;
// 匯出 RootState 和 AppDispatch 的類型(如果用 TypeScript,這裡可以忽略)
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
現在,我們需要在 src/index.js(或 src/main.jsx 如果是 Vite 專案)裡,用 <Provider> 來包住整個應用,讓所有元件都能存取 store。
完整程式碼(src/index.js):
import React from "react";
import ReactDOM from "react-dom/client"; // 如果是 React 18,用這個;舊版用 ReactDOM.render
import "./index.css";
import App from "./App";
import { Provider } from "react-redux";
import store from "./store"; // 剛建立的 store
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
現在 store 就設定好了!但還沒有任何狀態,我們需要建立 slice(一個 slice 就是一塊狀態的管理單元)。
3. 使用 createSlice 建立 Slice(狀態管理單元)
Slice 是 RTK 的核心,它用 createSlice 一次建立 reducer 和 action creators。假設我們要做一個簡單的計數器(counter),當使用者點擊按鈕時,數字加 1 或減 1。
步驟:
-
在 src 資料夾下建立 features 資料夾(這是官方推薦的資料夾結構,用來放 slice)。
-
在 src/features 裡建立 counter 資料夾。
-
在 src/features/counter 裡建立 counterSlice.js 檔案。
完整程式碼(src/features/counter/counterSlice.js):
import { createSlice } from "@reduxjs/toolkit";
// 定義初始狀態
const initialState = {
value: 0, // 計數器的初始值
};
// 建立 slice
const counterSlice = createSlice({
name: "counter", // slice 的名稱,用來產生 action type,例如 'counter/increment'
initialState, // 初始狀態
reducers: {
// reducers 裡定義如何更新狀態
// 加 1 的 reducer
increment: (state) => {
// 這裡可以用「可變式」寫法,因為 RTK 內部用 Immer 處理成不可變更新
state.value += 1;
},
// 減 1 的 reducer
decrement: (state) => {
state.value -= 1;
},
// 加指定數字的 reducer(示範帶 payload)
incrementByAmount: (state, action) => {
state.value += action.payload; // action.payload 是傳入的數字
},
},
});
// 從 slice 匯出 action creators(用來 dispatch action)
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
// 匯出 reducer(用來加到 store)
export default counterSlice.reducer;
解釋:
-
name:用來產生唯一的 action type,避免衝突。
-
initialState:這塊狀態的起始值。
-
reducers:一個物件,每個 key 就是一個 action type。函式參數 state 是當前狀態,action 是觸發的 action(如果有 payload,就在 action.payload 裡)。
-
因為用 Immer,你可以直接改 state.value,不用寫
{ ...state, value: state.value + 1 }這種不可變寫法。
現在,把這個 reducer 加到 store 裡。修改 src/store/index.js:
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "../features/counter/counterSlice"; // 匯入剛建立的 reducer
const store = configureStore({
reducer: {
counter: counterReducer, // 把 counter slice 加到 store,狀態會變成 { counter: { value: 0 } }
},
});
export default store;
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
4. 在 React 元件中使用 Store(用 Hooks)
在 React 元件裡,我們用 useSelector 來讀取狀態,用 useDispatch 來發送 action。假設在 App.js 裡做一個計數器介面。
完整程式碼(src/App.js):
import React from "react";
import { useSelector, useDispatch } from "react-redux"; // 匯入 hooks
import {
increment,
decrement,
incrementByAmount,
} from "./features/counter/counterSlice"; // 匯入 action creators
import "./App.css";
function App() {
// useSelector 讀取狀態:state.counter.value
const count = useSelector((state) => state.counter.value);
// useDispatch 取得 dispatch 函式
const dispatch = useDispatch();
return (
<div className="App">
<h1>計數器:{count}</h1>
<button onClick={() => dispatch(increment())}>加 1</button>
<button onClick={() => dispatch(decrement())}>減 1</button>
<button onClick={() => dispatch(incrementByAmount(5))}>加 5</button>
</div>
);
}
export default App;
步驟解釋:
-
useSelector((state) => state.counter.value):從 store 取出 counter.value。state 是整個 store 的狀態物件。
-
useDispatch():回傳一個 dispatch 函式,用來發送 action。
-
在按鈕的 onClick 裡,呼叫 dispatch(actionCreator()) 來觸發更新。
-
狀態改變後,元件會自動重新渲染(因為 useSelector 監聽變化)。
執行 npm start,你就能看到計數器運作了!在瀏覽器開發工具的 Redux 面板,能看到 action 歷史和狀態變化。
5. 處理非同步操作(用 createAsyncThunk)
RTK 內建 Redux Thunk,讓你能處理 API 請求等非同步邏輯。用 createAsyncThunk 來建立一個 thunk action,它會自動產生 pending、fulfilled、rejected 三個狀態。
假設我們要從 API 抓一個隨機使用者資料,加到狀態裡。
修改 src/features/counter/counterSlice.js,新增一個 async thunk(我們用 counter slice 來示範,但實際上可以單獨一塊):
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
const initialState = {
value: 0,
user: null, // 新增使用者狀態
loading: false, // 載入中狀態
error: null, // 錯誤狀態
};
// 建立 async thunk:抓使用者資料
export const fetchUser = createAsyncThunk(
"counter/fetchUser", // action type prefix
async () => {
const response = await fetch("https://randomuser.me/api/");
if (!response.ok) {
throw new Error("抓資料失敗!");
}
const data = await response.json();
return data.results[0]; // 回傳使用者資料
}
);
const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
// extraReducers 處理 async thunk 的狀態
extraReducers: (builder) => {
builder
// 當 fetchUser pending 時
.addCase(fetchUser.pending, (state) => {
state.loading = true;
state.error = null;
})
// 當 fetchUser fulfilled 時(成功)
.addCase(fetchUser.fulfilled, (state, action) => {
state.loading = false;
state.user = action.payload; // 存入使用者資料
})
// 當 fetchUser rejected 時(失敗)
.addCase(fetchUser.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
});
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
在 App.js 裡使用:
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import {
increment,
decrement,
incrementByAmount,
fetchUser,
} from "./features/counter/counterSlice";
function App() {
const count = useSelector((state) => state.counter.value);
const user = useSelector((state) => state.counter.user);
const loading = useSelector((state) => state.counter.loading);
const error = useSelector((state) => state.counter.error);
const dispatch = useDispatch();
useEffect(() => {
// 載入時抓使用者資料
dispatch(fetchUser());
}, [dispatch]);
return (
<div className="App">
<h1>計數器:{count}</h1>
<button onClick={() => dispatch(increment())}>加 1</button>
<button onClick={() => dispatch(decrement())}>減 1</button>
<button onClick={() => dispatch(incrementByAmount(5))}>加 5</button>
<h2>使用者資料:</h2>
{loading && <p>載入中...</p>}
{error && <p>錯誤:{error}</p>}
{user && (
<div>
<p>
姓名:{user.name?.first} {user.name?.last}
</p>
<img src={user.picture?.medium} alt="使用者頭像" />
</div>
)}
</div>
);
}
export default App;
解釋:
-
createAsyncThunk:第一個參數是 type prefix,第二個是 async 函式,回傳 Promise。
-
extraReducers:用 builder 來處理 thunk 的三種狀態(pending/fulfilled/rejected)。
-
在元件裡,用 useEffect dispatch thunk,就能在狀態中看到 loading、user 或 error。
6. 進階:RTK Query(資料請求與快取)
RTK 還有一個超強的功能叫 RTK Query,用來處理 API 請求和快取,不用手寫 thunk。它自動產生 hooks 和狀態管理。
安裝(已經包含在 RTK 裡,不用額外裝)。
範例:建立一個 API slice 來抓 todos 清單。
在 src/features 建立 todosApi.js:
javascript
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
export const todosApi = createApi({
reducerPath: "todosApi", // 在 store 中的路徑
baseQuery: fetchBaseQuery({
baseUrl: "https://jsonplaceholder.typicode.com/",
}), // API 基底 URL
endpoints: (builder) => ({
// 定義 getTodos 端點
getTodos: builder.query({
query: () => "todos", // 請求路徑
}),
}),
});
// 自動產生 hooks
export const { useGetTodosQuery } = todosApi;
加到 store(修改 src/store/index.js):
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "../features/counter/counterSlice";
import { todosApi } from "../features/todosApi"; // 匯入 API
const store = configureStore({
reducer: {
counter: counterReducer,
[todosApi.reducerPath]: todosApi.reducer, // 加 API reducer
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(todosApi.middleware), // 加 API middleware
});
export default store;
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
在 App.js 使用:
// ... 其他程式碼 ...
import { useGetTodosQuery } from "./features/todosApi";
function App() {
// ... 其他 ...
const { data: todos, isLoading, error } = useGetTodosQuery(); // 自動處理 loading 和 error
return (
<div className="App">
{/* ... 計數器 ... */}
<h2>Todos 清單:</h2>
{isLoading && <p>載入中...</p>}
{error && <p>錯誤:{error}</p>}
{todos && (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)}
</div>
);
}
export default App;
RTK Query 會自動快取資料、下次請求時直接用快取,避免重複 API 呼叫。超方便!