Redux Toolkit: How to refer action created with createAction as type

Viewed 6353

Redux toolkit provides createAction helper method. I am having trouble referring actions created with createAction as type.

For example:

const getData = createAction<PayloadAction<number>>('user/getData')

function* exampleSaga(action: ?) {
   ...
}

How can I tell exampleSaga that action is of type getData?



To see how I've tried doing it, refer the below (example) code and see comments for the problem I am facing:

reducer.ts

import { createAction, PayloadAction } from '@reduxjs/toolkit';

export const getData = createAction<PayloadAction<number>>('user/getData');

const userSlice = createSlice({
  name: userSliceName,
  initialState,
  reducers: {
    getUser(state: IUserState): void {
      state.loading = true;
      state.error = null;
    },
    getUserSuccess(state: IUserState, action: PayloadAction<{ user: IUser }>): void {
      const { user } = action.payload;
      state.user = user;
      state.loading = false;
      state.error = null;
    },
    getUserFailure(state: IUserState, action: PayloadAction<string>): void {
      state.loading = false;
      state.error = action.payload;
    },
  },
});

export type UserSaga = Generator<
  | CallEffect<IUser>
  | PutEffect<typeof getUserSuccess>
  | PutEffect<typeof getUserFailure>
  | ForkEffect<never>
>;

sagas.ts

import { UserSaga, getData } from './reducer.ts';

// For the following fetchUser saga arguments, how can I define that 'action' is of type getData, imported from the above 'reducer.ts' file.
function* fetchUser(action: PayloadAction<typeof getData>): UserSaga {
  try {
    const userId = action.payload;
    // Problem is here
    // I expect userId to be of type number, whereas it is:
    // ActionCreatorWithPayload<{payload: number, type: string}>
    ...
  } catch (e) {
    ...
  }
}

function* userSaga(): UserSaga {
  const pattern: any = getData.type;
  yield takeLatest(pattern, fetchUser);
}
2 Answers

This is how you will get it using return type of your getData.

function* exampleSaga(action: ReturnType<typeof getData>) {

...

}

I did some more digging and here's how I got around the issue.

An action could be created without tying it to a SliceCaseReducer: (Note that the created getData below is not an Action per se rather an ActionCreator).

common.ts

import { createAction } from '@reduxjs/toolkit';

export const getData = createAction<number>('getData');

getData is already cooked to be dispatched from a FunctionComponent if given the correct payload:

component.tsx

import React, { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { useParams } from 'react-router';
import { getData } from './state/common';

const User: React.FC = (): React.ReactElement => {
  const { userId } = useParams();
  const dispatch = useDispatch();

  useEffect((): void => {
    dispatch(getData(Number(userId)));
  }, []);

  return (<>User Component</>);
};

export default UserContainer;

With Sagas

To have a saga listen to the action

Saga Effects accept string type of an action to react. Luckily, actionCreators expose a type property representing the defined string name of the action.

saga.ts (listener)

import { takeLatest } from 'redux-saga/effects';
import { getData } from './common';

function* userSaga() {
  // getData.type could be input to a Saga Effect
  yield takeLatest(getData.type, fetchUser); // fetchUser is a worker saga explained below
}

Specifying action as argument of a saga.

Specifying the action as argument to a saga is a bit tricky, we have to use TypeScript's Type Guards. Interestingly, it's mentioned in the redux-toolkit's documentation as well. In short, we have to use actionCreator.match method of the actionCreator to discriminate down the passed action to the desired type. Thus, after discrimination, we receive the desired static typing for the matched action's payload.

saga.ts (worker)

import { Action } from '@reduxjs/toolkit';
import { call, put } from 'redux-saga/effects';
import * as userApi from '../../../api/user-api';
import { getData } from './common';
import { getUserSuccess, getUserFailure } from './user';

// Note that we can't directly specify that fetchUser only accepts getData action type.
function* fetchUser(action: Action) {
  try {
    // getData.match could be used to assert that we are only concerned with getData action type
    if (getData.match(action)) {
      // Inside the body of if, we get correct static typing
      const userId = action.payload;

      const fetchedUser = yield call(userApi.getUser, userId);
      yield put(getUserSuccess({user: fetchedUser}));
    }
  } catch (e) {
    yield put(getUserFailure(e.message));
  }
}
Related