Skip to main content

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;

步驟解釋:

  1. useSelector((state) => state.counter.value):從 store 取出 counter.value。state 是整個 store 的狀態物件。

  2. useDispatch():回傳一個 dispatch 函式,用來發送 action。

  3. 在按鈕的 onClick 裡,呼叫 dispatch(actionCreator()) 來觸發更新。

  4. 狀態改變後,元件會自動重新渲染(因為 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 呼叫。超方便!