Redux Toolkit
以下範例都會使用 TypeScript 講解。
安裝
- npm
- yarn
npm install redux react-redux @reduxjs/toolkit
yarn add 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 一樣:
在父層元件使用 Provider 匯入 store
App.tsximport 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;透過剛剛匯出的 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 上獲取。