React 系列 : Redux + TypeScript

React 系列 : Redux + TypeScript

  • 前言
  • 正文
    • Store
    • Actions
      • 基本同步 actions
      • 異步 actions + TS
        • ThunkAction 類型
          • TReturnType
          • TState
          • TExtraThunkArg
          • TBasicAction
          • inner function
    • Reducer
    • React + Redux
    • TodoApp 效果展示
  • 結語
  • 參考

前言

前幾天寫過一篇關於 react + TS 的簡單入門,大致上就是說說 TS 的強大以及基本怎麼跟 react 配合起來,若不清楚歡迎先去看看 React 搭配 TypeScript 新手入門。這篇算是接著談談,redux + TS 怎麼配合,分享一些寫法等等。

正文

接下來這篇會用 redux + TS 做一個相對完整一點的 todo 應用,本篇嘗試使用一個新的組件庫 chakra UI,聽說是個相對新的組件庫,個人覺得挺酷的想來試試看。

首先為了更好的組織代碼,我們先在 /src 下新建一個 store 文件夾,存放所有與狀態管理相關 redux 的代碼。

Store

因為本篇主要想延續上一篇說說 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)。

Actions

首先我們先分析一下這個 todo 應用想完成哪些功能,我完成的基本功能如下:

  • addTodo: 添加一個 todo
  • deleteTodo: 刪除一個 todo
  • updateTodo: 更新一個 todo
  • toggleTodo: 勾起一個 todo,為勾起表示還沒完成
  • setNewTodo: 設置全局 store 中的 newTodo
  • setTodos: 設置一組 todo
  • loadTodos: 從第三方加載一些 todo(s)

基本同步 actions

我接著在 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 怎麼配合去做異步的調用。

異步 actions + TS

其實在之前 Redux 中間件以及異步 action也提到過 redux 中對於異步調用的方法,主要就是通過一個 redux-thunk 中間件做到讓我們的 action 不僅能夠返回一個 js 對象,也能返回一個 function。

所以這邊一樣是使用這樣的方法,不過因為配合上了 TS,所以我們還必須加上該函數的返回類型,當然我們就是要來解釋下這個返回類型 ThunkAction> 是什麼鬼。

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
  • TReturnType: 這個參數就是要填你這個 thunk 的返回類型,而我們返回的是一個 async 函數,而在 TS 中是沒有 function 類型的,我們可以用 void 類型作為無論是一般函數還是 async 函數的標籤,所以第一個參數填 void。
TState
  • TState: 這個參數填的就是你這個 action 對應的 redux 的 state 類型。
TExtraThunkArg
  • TExtraThunkArg: 這個參數是你可以填一些額外的參數,會被傳到你內部函數中。如果沒有則默認就是填 unknown。
TBasicAction
  • 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

inner function

可以看到其實 ThunkAction 的返回值又是一個函數,而這個函數就是我們返回的 inner function。所以在我們的代碼中,async 函數的參數才有一個 dispatch 給我們用。

dispatch 可以讓我們分發其他 non-thunk action,getState 則根據 ThunkActionTState 參數可以獲取你這個 action 對應的 redux 的 state 的數據,而 extraArgument 則接了 ThunkActionTExtraThunkArg 參數。

Reducer

接下來開發我們的 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 加上了很多類型檢查。

React + Redux

最後我們要做的當然就是將 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>
  );
}

TodoApp 效果展示

React 系列 : Redux + TypeScript_第1张图片

具體代碼若有需要於 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/

你可能感兴趣的:(React,react,typescript,redux,redux-thunk)