前幾天寫過一篇關於 react + TS 的簡單入門,大致上就是說說 TS 的強大以及基本怎麼跟 react 配合起來,若不清楚歡迎先去看看 React 搭配 TypeScript 新手入門。這篇算是接著談談,redux + TS 怎麼配合,分享一些寫法等等。
接下來這篇會用 redux + TS 做一個相對完整一點的 todo 應用,本篇嘗試使用一個新的組件庫 chakra UI,聽說是個相對新的組件庫,個人覺得挺酷的想來試試看。
首先為了更好的組織代碼,我們先在 /src 下新建一個 store 文件夾,存放所有與狀態管理相關 redux 的代碼。
因為本篇主要想延續上一篇說說 TS 配合 redux,所以我們新建一個 types.ts 文件,專門用來定義狀態類型。
// store/types.ts
export interface Todo {
id: number;
text: string;
done: boolean;
}
export interface Store {
todos: Todo[];
newTodo: string;
}
我的一個 Todo 包含一個 id,text 表示 todo 描述,done 表示完成了沒。
而我們的 Store 類型則包含很多個 todo 以及一個 newTodo 用於存儲新增 todo 時的綁定變量,其實後面我發現這個 newTodo 其實更適合直接放在添加 todo 的組件中,但為了演釋一下 redux,我不想讓 Store 類型只有一個東西(hhh)。
首先我們先分析一下這個 todo 應用想完成哪些功能,我完成的基本功能如下:
addTodo
: 添加一個 tododeleteTodo
: 刪除一個 todoupdateTodo
: 更新一個 todotoggleTodo
: 勾起一個 todo,為勾起表示還沒完成setNewTodo
: 設置全局 store 中的 newTodosetTodos
: 設置一組 todoloadTodos
: 從第三方加載一些 todo(s)我接著在 store 下面新建 actions 文件夾,放 redux 所需要的 actions 的代碼。
// /store/actions.ts
import {
Todo, Store } from "./types";
import axios from "axios";
import {
ThunkAction } from "redux-thunk";
import {
Action } from "redux";
export const ADD_TODO = "ADD_TODO";
export const DELETE_TODO = "DELETE_TODO";
export const UPDATE_TODO = "UPDATE_TODO";
export const TOGGLE_TODO = "TOGGLE_TODO";
export const SET_NEWTODO = "SET_NEWTODO";
export const SET_TODOS = "SET_TODOS";
export const LOAD_TODOS = "LOAD_TODOS";
export type ActionTypes =
| {
type: typeof ADD_TODO }
| {
type: typeof DELETE_TODO; payload: number }
| {
type: typeof UPDATE_TODO;
payload: {
id: number;
text: string;
};
}
| {
type: typeof TOGGLE_TODO; payload: number }
| {
type: typeof SET_NEWTODO; payload: string }
| {
type: typeof SET_TODOS; payload: Todo[] }
| {
type: typeof LOAD_TODOS; payload: string };
// action creators
export const addTodo = (): ActionTypes => {
return {
type: ADD_TODO,
};
};
export const deleteTodo = (id: number): ActionTypes => {
return {
type: DELETE_TODO,
payload: id,
};
};
export const updateTodo = (id: number, text: string): ActionTypes => {
return {
type: UPDATE_TODO,
payload: {
id,
text,
},
};
};
export const toggleTodo = (id: number): ActionTypes => {
return {
type: TOGGLE_TODO,
payload: id,
};
};
export const setNewTodo = (text: string): ActionTypes => {
return {
type: SET_NEWTODO,
payload: text,
};
};
export const setTodos = (todos: Todo[]): ActionTypes => {
return {
type: SET_TODOS,
payload: todos,
};
};
export const loadTodos =
(url: string): ThunkAction<void, Store, unknown, Action<string>> =>
async (dispatch) => {
const res = await axios.get(url);
const todos: Todo[] = res.data;
dispatch(setTodos(todos));
};
其實我們也可以看到,其實大部分跟 redux action 的概念幾乎一模一樣,只不過配合上 TS 加了很多類型的檢查。不過會注意到 loadTodos
的寫法好像不太一樣,所以這邊主要說明一下 redux + TS 怎麼配合去做異步的調用。
其實在之前 Redux 中間件以及異步 action也提到過 redux 中對於異步調用的方法,主要就是通過一個 redux-thunk 中間件做到讓我們的 action 不僅能夠返回一個 js 對象,也能返回一個 function。
所以這邊一樣是使用這樣的方法,不過因為配合上了 TS,所以我們還必須加上該函數的返回類型,當然我們就是要來解釋下這個返回類型 ThunkAction
是什麼鬼。
我們從源碼來看看這四個參數的含義:
* @template TReturnType The return type of the thunk's inner function
* @template TState The redux state
* @template TExtraThunkARg Optional extra argument passed to the inner function
* (if specified when setting up the Thunk middleware)
* @template TBasicAction The (non-thunk) actions that can be dispatched.
*/
export type ThunkAction<
TReturnType,
TState,
TExtraThunkArg,
TBasicAction extends Action
> = (
dispatch: ThunkDispatch<TState, TExtraThunkArg, TBasicAction>,
getState: () => TState,
extraArgument: TExtraThunkArg,
) => TReturnType;
可以看到源碼也是用的 TS,ThunkAction
就是一個類型,而這個類型又要求四個參數。
TReturnType
: 這個參數就是要填你這個 thunk 的返回類型,而我們返回的是一個 async 函數,而在 TS 中是沒有 function 類型的,我們可以用 void 類型作為無論是一般函數還是 async 函數的標籤,所以第一個參數填 void。TState
: 這個參數填的就是你這個 action 對應的 redux 的 state 類型。TExtraThunkArg
: 這個參數是你可以填一些額外的參數,會被傳到你內部函數中。如果沒有則默認就是填 unknown。TBasicAction
: 這個參數填的是要被 dispatch 的非 thunk action 類型。那我們看到 TBasicAction
繼承自 Action
,來看看 Action
的源碼: * @template T the type of the action's `type` tag.
*/
export interface Action<T = any> {
type: T
}
其實 Action 在底層就是一個 TS 接口,唯一的屬性就是一個 type,而這個 type 其實可以是任意類型的(any
)。
所以看到這邊我們就明白為什麼要填 Action
了。因為這個參數要填的就是要被 dispatch 的 action 類型,而在 redux 中的 action 的 type 都是 string 類型的,所以才要填 Action
。
可以看到其實 ThunkAction
的返回值又是一個函數,而這個函數就是我們返回的 inner function。所以在我們的代碼中,async 函數的參數才有一個 dispatch 給我們用。
dispatch
可以讓我們分發其他 non-thunk action,getState
則根據 ThunkAction
的 TState
參數可以獲取你這個 action 對應的 redux 的 state 的數據,而 extraArgument
則接了 ThunkAction
的 TExtraThunkArg
參數。
接下來開發我們的 reducer,也就是實際處理狀態轉換的核心,直接看看代碼應該沒什麼問題的:
// /store/reducers.js
import {
ActionTypes,
DELETE_TODO,
SET_TODOS,
SET_NEWTODO,
UPDATE_TODO,
ADD_TODO,
TOGGLE_TODO,
} from "./actions";
import {
Store, Todo } from "./types";
export const updateTodo = (todos: Todo[], id: number, text: string): Todo[] =>
todos.map((todo) => ({
...todo,
text: todo.id === id ? text : todo.text,
}));
export const toggleTodo = (todos: Todo[], id: number): Todo[] =>
todos.map((todo) => ({
...todo,
done: todo.id === id ? !todo.done : todo.done,
}));
export const deleteTodo = (todos: Todo[], id: number): Todo[] =>
todos.filter((todo) => todo.id !== id);
export const addTodo = (todos: Todo[], text: string): Todo[] => [
...todos,
{
id: Math.max(0, Math.max(...todos.map(({
id }) => id))) + 1,
text,
done: false,
},
];
// Redux reducer
export function todoReducer(
state: Store = {
todos: [], newTodo: "" },
action: ActionTypes
) {
switch (action.type) {
case SET_TODOS:
return {
...state,
todos: action.payload,
};
case DELETE_TODO:
return {
...state,
todos: deleteTodo(state.todos, action.payload),
};
case SET_NEWTODO:
return {
...state,
newTodo: action.payload,
};
case UPDATE_TODO:
return {
...state,
todos: updateTodo(state.todos, action.payload.id, action.payload.text),
};
case ADD_TODO:
return {
...state,
newTodo: "",
todos: addTodo(state.todos, state.newTodo),
};
case TOGGLE_TODO:
return {
...state,
todos: toggleTodo(state.todos, action.payload),
};
default:
return state;
}
}
這邊就跟一般寫 reducer 沒有太多區別,就是配合著 TS 加上了很多類型檢查。
最後我們要做的當然就是將 redux 裝配到我們的 react 應用中,讓上面開發的數據源變成全局共享的頂層數據源。這邊也跟之前一樣。
// /store/todosStore
import {
createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import {
todoReducer } from "./reducers";
const todosStore = createStore(todoReducer, applyMiddleware(thunk));
export default todosStore;
// /App.tsx
import * as React from "react";
import {
ChakraProvider, Box, theme } from "@chakra-ui/react";
import TopBar from "./components/TopBar";
import TodoList from "./components/TodoList";
import TodoAdd from "./components/TodoAdd";
import {
Provider } from "react-redux";
import todosStore from "./store/todosStore";
export function App() {
return (
<Provider store={
todosStore}>
<ChakraProvider theme={
theme}>
<Box maxWidth="8xl" margin="auto" p={
5}>
<TopBar />
<TodoList />
<TodoAdd />
</Box>
</ChakraProvider>
</Provider>
);
}
具體代碼若有需要於 https://github.com/cclintris/TodoApp_ReactReduxTS 獲取。
本篇大致上介紹了怎麼在 react + TS 的項目中使用 redux,其實跟之前的用法概念也都大同小異,唯一的不同在於 redux 的異步 action 要怎麼跟 TS 配合起來,這部分也通過源碼的方式詳細的做了理解分享。不過有點可惜的是還是沒有很了解 chakra UI 怎麼用,之後還多逛逛 chakra UI 官網,自己在項目中有機會試試看qwq。
本篇僅僅是分享一些對於 react + TS + redux 的個人理解,若有錯誤或是不完整也歡迎大老們多多指教~
參考 | 鏈接 |
---|---|
How to properly type a thunk with ThunkAction using redux-thunk in Typescript? | https://stackoverflow.com/questions/63881398/how-to-properly-type-a-thunk-with-thunkaction-using-redux-thunk-in-typescript?noredirect=1 |
redux-thunk 源碼 | https://github.com/reduxjs/redux-thunk/blob/master/src/index.d.ts |
Mastering Typescript State Management using Redux | https://www.youtube.com/watch?v=emhwHjAsyss |
chakra 官方 | https://chakra-ui.com/ |