Реагирует на наблюдаемую эпопею с помощью Redux Toolkit и Typescript

#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;