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 呼叫。超方便!