使用 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 操作,无论是使用 createAction
、createSlice
还是 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;
我不确定如何使用 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
:
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 操作,无论是使用 createAction
、createSlice
还是 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;