使用 Redux Toolkit 和 Typescript 反应可观察史诗

React observable epic with Redux Toolkit and Typescript

我不确定如何使用 Redux Toolkit 和 Typescript 编写 React 可观察史诗。

假设我有这个 authSlice:

import { CaseReducer, createSlice, PayloadAction } from "@reduxjs/toolkit";

type AuthState = {
  token: string,
  loading: boolean,
};

const initialState: AuthState = {
  token: "",
  loading: false,
};

const loginStart: CaseReducer<AuthState, PayloadAction<{username: string, password: string}>> = (state, action) => ({
  ...state,
  loading: true,
  token: "",
});

const loginCompleted: CaseReducer<AuthState, PayloadAction<{token: string}>> = (state, action) => ({
  ...state,
  loading: false,
  token: action.payload.token,
});

const authSlice = createSlice({
  name: 'auth',
  initialState,
  reducers: {
    loginStart,
    loginCompleted,
  },
});

export default authSlice;

这家店:

import { configureStore } from '@reduxjs/toolkit';
import { combineEpics, createEpicMiddleware } from 'redux-observable';
import authEpic from './epics/authEpic';
import authSlice from './slices/authSlice';

const epicMiddleware = createEpicMiddleware();

export const rootEpic = combineEpics(
  authEpic
);

const store = configureStore({
  reducer: {
    auth: authSlice.reducer,
  },
  middleware: [epicMiddleware]
});

epicMiddleware.run(rootEpic);

export type RootState = ReturnType<typeof store.getState>;
export default store;

这个authEpic应该怎么写(希望目的是self-explanatory):

import { Action, Observable } from 'redux';
import { ActionsObservable, ofType } from 'redux-observable';
import { ajax } from 'rxjs/ajax';
import { switchMap } from 'rxjs/operators';
import authSlice from '../slices/authSlice';

export default (action$: ActionsObservable<???>) => action$.pipe(
  ofType(???), /* should be of type loginStart */
  switchMap<???,???>(action => ajax.post( // should be from a loginStart action to {token: string}
    "url", {
      username: action.payload.username, 
      password: action.payload.password 
    }
  )),
  ...
);

我对 ???这就是类型应该是什么以及 redux observable 应该如何与 redux 工具包链接。

有什么提示吗?

问题是 redux-toolkit 模糊了动作,所以很难知道动作类型是什么。而在传统的 redux 设置中,它们只是一堆常量。

type T = ReturnType<typeof authSlice.actions.loginStart>['type']; // T is string

// have to create an action to find the actual value of the string
const action = authSlice.actions.loginStart({username: "name", password: "pw"});
const type = action.type;
console.log(type);

authSlice.actions.loginStart 创建的操作的 action.type 似乎是“auth/loginStart”,其类型只是 string 而不是特定的字符串文字。公式为${sliceName}/${reducerName}。所以 ofType 变成

ofType("auth/loginStart")

现在是通用注释。我们的 authEpic 正在执行登录开始操作并将其转换为登录完成操作。我们可以通过查看 authSlice:

以 round-about 的方式获得这两种类型
type LoginStartAction = ReturnType<typeof authSlice.actions.loginStart>`)

但这很愚蠢,因为我们在创建 authSlice 时就已经知道动作类型了。操作类型是 CaseReducer 中的 PayloadAction。让我们别名并导出它们:

export type LoginStartAction = PayloadAction<{ username: string; password: string }>;

export type LoginCompletedAction = PayloadAction<{ token: string }>;

这些是您将用于 case reducers 的类型:

const loginStart: CaseReducer<AuthState, LoginStartAction> = ...

const loginCompleted: CaseReducer<AuthState, LoginCompletedAction> = ...

我不太熟悉 observables 和 epics,但我认为您想要在 authEpic 上输入的内容是:

export default (action$: ActionsObservable<LoginStartAction>) => action$.pipe(
    ofType("auth/loginStart"),
    switchMap<LoginStartAction, ObservableInput<LoginCompletedAction>>(
        action => ajax.post(
            "url", action.payload
        )
    )
    ...
);

在 redux-toolkit 中,您应该在 filter 中使用 action.match 函数而不是 ofType 来实现类似的工作流程,如 stated in the documentation.

文档中的这个示例将适用于所有 RTK 操作,无论是使用 createActioncreateSlice 还是 createAsyncThunk.

创建的
import { createAction, Action } from '@reduxjs/toolkit'
import { Observable } from 'rxjs'
import { map, filter } from 'rxjs/operators'

const increment = createAction<number>('INCREMENT')

export const epic = (actions$: Observable<Action>) =>
  actions$.pipe(
    filter(increment.match),
    map((action) => {
      // action.payload can be safely used as number here (and will also be correctly inferred by TypeScript)
      // ...
    })
  )

我通读了 redux-toolkit 文档并尝试尽我所能将其应用于 redux-observable。这是我想出来的。

import { delay, mapTo} from 'rxjs/operators';
import { ofType } from 'redux-observable';
import { createSlice} from "@reduxjs/toolkit";

const delayTime = 1000
export type pingValues = 'PING' | 'PONG'

export interface PingState {
    value: pingValues,
    isStarted: boolean,
    count: number
}

const initialState: PingState = {
    value: 'PING',
    isStarted: false,
    count: 0
};

export const pingSlice = createSlice({
    name: 'ping',
    initialState,
    reducers: {
        // createSlice does some cool things here. It creates an Action Create function (setPing()) and an Action Type, with a type property 'ping/setPing'. It adds that string as ToString() on the function as well which we can use in the ofType() calls with rxjs
        setPing: (state => {
            state.value = 'PING'
            state.isStarted = true
            state.count++;
        }),
        setPong: (state => {
            state.value = 'PONG';
            state.isStarted = true;
            state.count++;
        })
    },
});

// Epics
export const pingEpic = (action$:any) => action$.pipe(
    ofType(setPing), // Pulling out the string 'ping/setPing' from the action creator 
    delay(delayTime),// Asynchronously wait 1000ms then continue
    mapTo(setPong()) // here we're executing the action creator to create an action Type 'plain old javascript object' 
);

export const pongEpic = (action$:any) => action$.pipe(
    ofType(setPong), 
    delay(delayTime),
    mapTo(setPing())
);
 

// Export the actionCreators
export const { setPing, setPong } = pingSlice.actions;

// export the reducer
export default pingSlice.reducer;