#reactjs #typescript #redux #redux-observable #redux-toolkit
#reactjs #typescript #redux #redux-наблюдаемый #redux-toolkit
Вопрос:
Я не уверен, как написать React observable epic с Redux Toolkit и Typescript.
Предположим, у меня есть этот 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 (надеюсь, цель понятна):
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 toolkit.
Какой-нибудь намек?
Ответ №1:
В redux-toolkit вы должны использовать action.match
функцию в a filter
вместо ofType
для аналогичного рабочего процесса, как указано в документации.
Этот пример из документации будет работать со всеми действиями 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)
// ...
})
)
Ответ №2:
Проблема в том, что 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);
Похоже, что action.type
для действия, созданного с помощью authSlice.actions.loginStart
, используется «auth / loginStart», а его тип — просто string
, а не конкретный строковый литерал. Формула такова ${sliceName}/${reducerName}
. Таким ofType
образом, становится
ofType("auth/loginStart")
Теперь для общих аннотаций. Мы authEpic
выполняем начальное действие входа в систему и преобразуем его в действие завершения входа в систему. Мы можем получить эти два типа обходным путем, посмотрев на authSlice
:
type LoginStartAction = ReturnType<typeof authSlice.actions.loginStart>`)
Но это глупо, потому что мы уже знаем типы действий с момента создания authSlice
. Тип действия находится PayloadAction
внутри вашего CaseReducer
. Давайте создадим псевдоним и экспортируем их:
export type LoginStartAction = PayloadAction<{ username: string; password: string }>;
export type LoginCompletedAction = PayloadAction<{ token: string }>;
Это типы, которые вы будете использовать для редукторов регистра:
const loginStart: CaseReducer<AuthState, LoginStartAction> = ...
const loginCompleted: CaseReducer<AuthState, LoginCompletedAction> = ...
Я не слишком знаком с наблюдаемыми и epics, но я думаю, что типы, которые вы хотите использовать authEpic
, следующие:
export default (action$: ActionsObservable<LoginStartAction>) => action$.pipe(
ofType("auth/loginStart"),
switchMap<LoginStartAction, ObservableInput<LoginCompletedAction>>(
action => ajax.post(
"url", action.payload
)
)
...
);
Комментарии:
1. Спасибо, только один вопрос: 1) есть ли способ избавиться от строки «auth / loginStart» и получить ее автоматически из authSlice.actions. loginStart?
2. Да, я тоже ненавижу жестко закодированные строки. Вы можете сделать это, создав фиктивное действие и получив доступ к его типу, но вам придется передать действительную полезную нагрузку для создания действия. Я только что просмотрел эту страницу документов redux-toolkit.js.org/usage/usage-with-typescript и, похоже, есть утилита, о которой я не знал, куда вы можете позвонить
loginStart.match(action)
, чтобы проверить, соответствует ли действие типу. Это не будет работать с вашимofType
каналом, но наверняка есть логический канал, который вы можете использовать для вызова этой функции.3. Да, я читал об этой
match
функции, однако она мне не нравится, потому что она просто проверяет тип полезной нагрузки, а не истинный тип действия AFAIK, поэтому, возможно, если у меня есть два действия с одним и тем же типом полезной нагрузки, я не уверен, что он будет делать… Тем не менее, я попробовал тривиально,ofType(authSlice.actions.loginStart.type)
и, похоже, это работает, вы согласны?4. Я бы подумал, что он проверяет тип действия? Но если
authSlice.actions.loginStart.type
работает, то да, сделайте это. Я не понимал, что у него естьtype
свойство, поскольку оно является основной функцией.5. Вы правы, как и @phry, я прочитал реализацию, и
filter(authSlice.actions.loginStart.match)
она действительно фильтрует тип действия и возвращает правильное введенное действие, так что это правильный путь!
Ответ №3:
Я прочитал документацию 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;