跳至主要内容

[Redux] Redux TypeScript 筆記

在 react-redux v7.2.3 後的版本,本來就會相依型別定義檔,開發者將可以不用自行下載型別定義。

TL;DR

Define Root State and Dispatch Types

import { Action, AnyAction, ThunkAction } from '@reduxjs/toolkit';

import { Epic } from 'redux-observable';
import { store } from '.';
import rootReducer from './reducers';

// define types for redux
// https://redux.js.org/usage/usage-with-typescript#define-root-state-and-dispatch-types
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;

// define types for redux-observable
export type AppState = ReturnType<typeof rootReducer>;
export type AppEpic = Epic<AnyAction, AnyAction, AppState>;

// define types for redux-thunk
// https://redux.js.org/usage/usage-with-typescript#type-checking-redux-thunks
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action<string>
>;

Define Typed Hooks:

// src/store/utils.ts
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
import { AppDispatch, RootState } from './types';

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

Project Setup

Define Root State and Dispatch Types

// app/store.ts
export const store = configureStore({
/* ... */
});

// define types for redux
// https://redux.js.org/usage/usage-with-typescript#define-root-state-and-dispatch-types
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;

建立帶有型別定義的 useDispatch 和 useSelector(Define Typed Hooks)

雖然可以在每個 Component 中直接使用定義好的 RootStateAppDispatch,但如果能建立帶有型別資訊的 useDispatchuseSelector 會更好,如此就:

  • 不用每次在元件中使用 useSelector 時還要額外寫 (state: RootState)
  • 不用每次在元件中使用 useDispatch 時,還要載入 AppDispatch 來讓 useDispatch 知道 middleware 的型別資訊
// app/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';

import type { AppDispatch, RootState } from './store';

// 之後在元件中,使用帶有型別資訊的 useDispatch 和 useSelector
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

後續在元件中需要使用原本的 useDispatchuseSelector 時,則都從 app/hook.ts 使用包過的 useAppDispatchuseAppSelector

ConfigureStore

Typing configureStore @ redux > Using Redux > Code Quality > Usage with TypeScript

configureStore 中,我們經常會添加額外的 middleware,建議使用 .concat().prepend() 方法來擴充需要的 middleware,而非 array spread,因為使用 array spread 有可能會遺失部分型別資訊:

// 透過 configureStore() 建立 Redux Store
export const store = configureStore({
reducer: rootReducer,
// 建議使用 concat 和 prepend,使用 array spread 可能會遺失型別資料
// https://redux.js.org/usage/usage-with-typescript#typing-configurestore
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(pokemonApi.middleware, epicMiddleware),
});

Application Usage

Define Slice State and Action Types

  • createSlice 中 state 的型別會被 initialState 的型別所決定,所以記得為 initialState 定義型別資訊
  • 如果某個 action 是帶有 payload 的,可以使用 PayloadAction<T> 中的 T 來定義 action.payload 的型別
// features/counter/counterSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

import { AppThunk, RootState } from '../../store/types';

/**
* 定義 initialState 的型別資訊,目的是讓 createSlice 知道該 slice 中 state 的型別資訊
**/
export interface CounterState {
value: number;
}

export const initialState: CounterState = {
value: 0,
};

export const counterSlice = createSlice({
name: 'counter',

// counterSlice 中,其 state 的型別會被 initialState 的型別所決定
initialState,
reducers: {
increment: (state) => ({
value: state.value + 1,
}),
decrement: (state) => ({
value: state.value - 1,
}),

// 使用 PayloadAction 型別來定義 action.payload 能夠接受的型別
// 這裡的 action.payload 需要時 number
incrementByAmount: (state, action: PayloadAction<number>) => ({
value: state.value + action.payload,
}),
},
});

Typing Additional Redux Logic

定義 AppThunk 這個 Type Utility:

// src/store/types.ts
import { Action, ThunkAction } from '@reduxjs/toolkit';

import { store } from '.';

// define types for redux
// https://redux.js.org/usage/usage-with-typescript#define-root-state-and-dispatch-types
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;

// define types for redux-thunk
// https://redux.js.org/usage/usage-with-typescript#type-checking-redux-thunks
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action<string>
>;

使用 AppThunk

// src/features/counter/counterSlice.ts

export const incrementIfOdd =
(amount: number): AppThunk =>
(dispatch, getState) => {
const currentValue = selectCount(getState());
if (currentValue % 2 === 1) {
setTimeout(() => dispatch(incrementByAmount(amount)), 1000);
}
};

Usage with Redux Toolkit

  • Typing configureStore:如何在 configureStore 中添加帶有型別資訊的 middlware

  • Matching Actions:透過 createAction 建立的 action creator,其 match 方法有 type predicate 的效果

  • Typing createSlice

    • 如果需要在 createSlice 外建立 reducers 的話,可以搭配使用型別 CaseReducer 來定義該 reducer
    • 如果需要使用 extraReducers 的話,建議搭配 builder.addCase() 使用,將能推導出正確的型別
    • 如果需要在 action 中添加 metaerror 屬性、或是客製化 payload,需要搭配 prrepare callback 使用
    • 若要解決 circular type dependency problem 的問題,可以使用 as Reducer<>
    export default counterSlice.reducer as Reducer<Counter>
  • Typing createAsyncThunk:如果需要修改 thunkApi 的型別,需要透過 generics 的參數自行調整

  • Typing createEntityAdapter:只需在 createEntityAdapter<T> 的 T 帶入 entity 的型別:

    // 在 createEntityAdapter 的 generics 中帶入泛型
    const booksAdapter = createEntityAdapter<Book>({
    selectId: book => book.bookId,
    sortComparer: (a, b) => a.title.localeCompare(b.title)
    })

    const booksSlice = createSlice({
    name: 'books',
    // 在 initialState 中,會自動根據 booksAdapter.getInitialState()
    // 來推導 slice 中 state 的型別
    initialState: booksAdapter.getInitialState(),
    reducers: {
    bookAdded: booksAdapter.addOne,
    booksReceived(state, action: PayloadAction<{ books: Book[] }>) {
    booksAdapter.setAll(state, action.payload.books)
    }
    }
    })

其他

  • Redux 官方強烈建議不要建立 unions of action types,