Skip to main content

Redux Toolkit

以下範例都會使用 TypeScript 講解。

安裝

npm install redux react-redux @reduxjs/toolkit

基本介紹

Redux Toolkit 可以更有效率地撰寫 Redux,它提供了許多 API 來建立 Store、Action 以及 Reducer。

以下會介紹如何使用這些 API 來協助我們快速建立 Redux:

createSlice()

slice 整合了 initialState、Reducer 及 Action,並根據其產生 Reducer 和 Action Creators。 以下為它的參數:

  • name:slice 的名稱。
  • initialState:state 的初始值。
  • reducers:根據 action 對 state 進行操作。
counterSlice.ts
import { createSlice } from '@reduxjs/toolkit';

export interface CounterState {
count: number;
}

const initialState: CounterState = {
count: 0;
};

// 這是一個計數器
const counter = createSlice({
name: 'counter', // slice 名稱
initialState, // state 的初始值
reducers: {
increment: (state, action) => {
state.count++; // 允許直接對 state 進行操作
},
decrement: (state, action) => {
state.count--;
},
addBy: (state, action) => {
state.count += action.payload;
}
},
});

// 將 Action Creators 及 Reducer 匯出
export const { increment, decrement, addBy } = counter.actions;
export const counterReducer = counter.reducer;

tip

在 Redux Toolkit 中附帶了 Immer,也就是說它允許開發人員直接更改狀態,不需要先複製再更改狀態。

configureStore()

configureStore() 會幫我們建立好一個 store。

store.ts
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import { counterReducer } from './counterSlice'; // 匯入剛剛的 Counter Reducer

const store = configureStore({
reducers: {
counter: counterReducer;
},
});

// 將 store 匯出
export default store;

// 在 TypeScript 中我們必須先定義好 Selector 及 Dispatch 的型別
type RootState = ReturnType<typeof store.getState>;
type AppDispatch = typeof store.dispatch;
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

使用方法與基本 Redux 一樣:

  1. 在父層元件使用 Provider 匯入 store

    App.tsx
    import Counter from './components/Counter';
    import { Provider } from 'react-redux';
    import store from './redux/store';

    function App() {
    return (
    <Provider store={store}>
    <Counter />
    </Provider>
    );
    }

    export default App;
  2. 透過剛剛匯出的 useAppSelector 及 useAppDispatch 來獲取或是操作 state

    Counter.tsx
    // 匯入 Action Creators
    import { increment, decrement, addBy } from '../redux/counterSlice';
    import { useAppSelector, useAppDispatch } from '../redux/store';

    function Counter() {
    const dispatch = useAppDispatch();
    const count = useAppSelector((state) => state.counter.count);

    return (
    <div>
    <h1>Counter</h1>
    <div>count: {count}</div>
    <button type="button" onClick={() => dispatch(increment())}></button> // 遞增
    <button type="button" onClick={() => dispatch(decrement())}></button> // 遞減
    <button type="button" onClick={() => dispatch(addBy(5))}></button> // 加
    </div>
    );
    }

    export default Counter;

非同步處理

在非同步處理的部分,Redux Toolkit 已經內建了 Redux Thunk,所以可以輕易地處理非同步請求。 以下不會示範整個 APP ,只截取建立 Thunk 及 Slice 的程式碼:

createAsyncThunk()

在 Redux Toolkit 中我們會使用這個 API 來建立一個 Thunk,而在 TypeScript 中我們必須提供 API 它的參數、回傳值的型別。

todoSlice
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios"; // 也可以使用內建的 fetch()

// 網路上別人建立測試用的 JSON API。
// 也有其他種格式,詳情可至 https://jsonplaceholder.typicode.com/ 查詢
const API_URL = 'https://jsonplaceholder.typicode.com/todos';

export interface Todo {
userId: number;
id: number;
title: string;
completed: boolean;
}

export interface TodoState {
todoList: Todo[] | undefined;
status: 'success' | 'error' | 'loading' | undefined;
message: string | undefined
}

// 從 API 獲取代辦事項
// <Todo[], void, { rejectValue: string }>
// Todo[] 為回傳的型別
// void 為非同步參數的型別 (這裡不須傳入參數所以設 void)
// { rejectValue: string } 錯誤處理的型別
// 最後一樣要將此匯出
export const fetchTodo = createAsyncThunk<Todo[], void, { rejectValue: string }>(
'todo/fetchTodo', async (_, { rejectWithValue }) => {
try {
const { data } = await axios.get('API_URL');
return data;
} catch (error: any) {
return rejectWithValue(error.message);
}
}
);

// 建立 Slice
const todo = createSlice({
name: 'todo',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchTodo.pending, (state, action) => {
state.todoList = undefined;
state.status = 'loading';
state.message = 'Loading.';
});
builder.addCase(fetchTodo.fulfilled, (state, action) => {
state.todoList = action.payload;
state.status = 'success';
state.message = 'fetch successful.';
});
builder.addCase(fetchTodo.rejected, (state, action) => {
state.todoList = undefined;
state.status = 'error';
state.message = action.payload;
});
},
});

export const todoReducer = todo.reducer;

接下來就是使用的方式了:

App.tsx
import { useEffect } from 'react';
import { useAppDispatch } from './redux/store';
import { fetchTodo } from './redux/todoSlice';

function App() {
const dispatch = useAppDispatch();
const todoList = useAppSelector(state => state.todo.todoList);

useEffect(() => {
dispatch(fetchTodo())
.unpack() // 若是要像普通的 Promise 使用 then() 及 catch() 可使用 unpack()
.then((data) => {
// 當 fetch 成功
// 其中 data 為 fetch 回傳值,也就是整個 Todo list
// 在這裡你可以做一些通知動畫等等的
})
.catch((error) => {
// 當 fetch 失敗
// 其中 error 為剛剛使用 rejectWithValue 的值
// 在這裡你可以做一些通知動畫等等的
});
}, []);

return (
<div>
<h1>Todo</h1>
{
todoList &&
todoList.map((todo) => {
return (
<div key={todo.id} style={{marginTop: '6px', padding: '4px', border: '1px solid #000'}}>
<div>userId: {todo.userId}</div>
<div>id: {todo.id}</div>
<div>title: {todo.title}</div>
<div>completed: {todo.completed}</div>
</div>
);
})
}
</div>
);
}

詳細範例可以至我的 Github 上獲取。

參考文章