How to dispatch ThunkAction with redux-thunk and TypeScript
Asked Answered
W

3

6

I am having trouble with dispatching a redux-thunk action using Typescript.

import { AnyAction, applyMiddleware, createStore } from 'redux'
import thunk, { ThunkAction } from 'redux-thunk'

interface State {
  counter: number
}

const initialState: State = {
  counter: 10
}

function reducer(state = initialState, action: AnyAction) {
  switch (action.type) {
    case 'increment':
      return { counter: action.payload }
    default:
      return state
  }
}

function increment(): ThunkAction<void, State, unknown, AnyAction> {
  return async function (dispatch) {
    dispatch({
      type: 'increment',
      payload: 20
    })
  }
}

const store = createStore(reducer, applyMiddleware(thunk))

store.dispatch(increment())

This is the error that I receive:

Argument of type 'ThunkAction<void, State, unknown, AnyAction>' is not assignable to parameter of type 'AnyAction'.
  Property 'type' is missing in type 'ThunkAction<void, State, unknown, AnyAction>' but required in type 'AnyAction'.

I have tried multiple different thinkgs for action type such as custom interface, Action etc. and nothing works.

Weighted answered 16/11, 2020 at 12:16 Comment(0)
P
9

The default dispatch type does not know of thunk, as the "base redux" types are not very powerful. So you'll have to cast it to ThunkDispatch manually:

(store.dispatch as ThunkDispatch<State, unknown, AnyAction>)(increment())

Just as a PSA: The type of redux you are writing here (vanilla redux with hand-written actions, action-types, switch-case statements and immutable logic in reducers) is no longer the "officially recommended approach" of writing redux. Please take a look at redux toolkit and best follow the official, up-to-date redux tutorials, as you are most likely following a very outdated one.

Redux Toolkit is also a lot easier to use in general, and specially with TypeScript (and store.dispatch will have the correct type if you use it ;) )

Peshitta answered 16/11, 2020 at 14:31 Comment(2)
This works. I just want to point out that I am using redux without React and as far I know redux-toolkit assumes you are using React.Weighted
@Weighted it does not. It is completely framework-agnostic.Peshitta
Q
2

Just a kick advise for those who are struggling with the dispatch function when using thunk and hooks.

Here is an example of what am I doing to manage authentication state, fetching data from graphql server. The magic is coming when defining the dispatch Type type IAppDispatch = ThunkDispatch<IAppState, any, IAppActions>;

store.ts

import { applyMiddleware, combineReducers, compose, createStore } from "redux";
import thunkMiddleware, { ThunkDispatch, ThunkMiddleware } from "redux-thunk";
import { authReducer } from "./reducers/authReducers";
import { IAuthActions } from "./types/authTypes";

const composeEnhancers =
    process.env.NODE_ENV === "development"
        ? (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
        : compose;

const rootReducer = combineReducers({
    authReducer,
});

type IAppActions = IAuthActions; <-- merge here other actions
type IAppState = ReturnType<typeof rootReducer>;
type IAppDispatch = ThunkDispatch<IAppState, any, IAppActions>; <--here is the magic

const reduxStore = createStore(
    rootReducer,
    composeEnhancers(
        applyMiddleware<IAppDispatch, any>(
            thunkMiddleware as ThunkMiddleware<IAppState, IAppActions, any>
        )
    )
);

export { reduxStore, IAppState, IAppDispatch, IAppActions };

authActions (actions creator and dispatch thunk actions)

import { Dispatch } from "redux";
import {
    loginMutation,
    logoutMutation,
} from "../../components/DataComponents/Authentification/fetchAuthentification";
import { GqlSessionUser } from "../../components/DataComponents/generatedTypes";
import {
    IAuthActions,
    IAuthErrorAction,
    IAuthLoadingAction,
    IAuthLoginAction,
    IAuthLogoutAction,
} from "../types/authTypes";

const authLogin = (appUserId: GqlSessionUser): IAuthLoginAction => {
    return {
        type: "AUTH_LOGIN",
        payload: {
            appUserId,
        },
    };
};

const authLogout = (): IAuthLogoutAction => {
    return {
        type: "AUTH_LOGOUT",
    };
};

const authLoadingAction = (isLoading: boolean): IAuthLoadingAction => {
    return {
        type: "AUTH_LOADING",
        payload: {
            isLoading,
        },
    };
};

const authErrorAction = (errorMessage: string): IAuthErrorAction => {
    return {
        type: "AUTH_ERROR",
        payload: {
            errorMessage,
        },
    };
};


const authLoginAction = (idOrEmail: string) => {
    return async (dispatch: Dispatch<IAuthActions>) => {
        dispatch(authLoadingAction(true));
        const { data, errors } = await loginMutation(idOrEmail); <--fetch data from GraphQl

        if (data) {
            dispatch(authLogin(data.login.data[0]));
        }
        if (errors) {
            dispatch(authErrorAction(errors[0].message));
        }

        dispatch(authLoadingAction(false));

        return true;
    };
};

const authLogoutAction = () => {
    return async (dispatch: Dispatch<IAuthActions>) => {
        dispatch(authLoadingAction(true));
        await logoutMutation(); <--fetch data from GraphQl
        dispatch(authLogout());
        dispatch(authLoadingAction(false));

        return true;
    };
};

export {
    authLoginAction,
    authLogoutAction,
    authLoadingAction,
    authErrorAction,
};

example of components that use state and dispatch async actions via useDispatch

please not how dispatch is typed as IAppDispatch, although it is imported from react-redux

    import React from "react";
import { useDispatch, useSelector } from "react-redux";
import {
    authLoginAction,
    authLogoutAction,
} from "../../../stateManagement/actions/authActions";
import { IAppDispatch, IAppState } from "../../../stateManagement/reduxStore";
import Button from "../../Button";

const Authentification: React.FC = (): JSX.Element => {
        const dispatch: IAppDispatch = useDispatch(); <--typing here avoid "type missing" error

        const isAuth = useSelector<IAppState>((state) => state.authReducer.isAuth);
    
        const authenticate = async (idOrEmail: string): Promise<void> => {
            if (!isAuth) {
                dispatch(authLoginAction(idOrEmail)); <--dispatch async action through thunk
            } else {
                dispatch(authLogoutAction()); <--dispatch async action through thunk
            }
        };
    
        return (
            <Button
                style={{
                    backgroundColor: "inherit",
                    color: "#FFFF",
                }}
                onClick={() => authenticate("[email protected]")}
            >
                {isAuth && <p>Logout</p>}
                {!isAuth && <p>Login</p>}
            </Button>
        );
    };
    export { Authentification };
Queasy answered 3/1, 2021 at 8:35 Comment(0)
M
0

I've faced the issue recently when tried to upgrade my app from HOC connect to use hooks. As I'm not using redux-toolkit (for historical reasons), it was a bit confusing how to use it all correctly with the typescript. The solution is based on some old create-react-app with typescript template. I have finished up with this what seems to be working:

store.ts

import { AnyAction } from 'redux';
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import { ThunkDispatch } from 'redux-thunk';

export interface ApplicationState { 
    sliceName: SliceType
    // other store slices
}

export interface AppThunkAction<TAction> {
    (dispatch: (action: TAction) => void, getState: () => ApplicationState): void;
}

export const useStoreSelector: TypedUseSelectorHook<ApplicationState> = useSelector;
export const useStoreDispatch = () => useDispatch<ThunkDispatch<ApplicationState, unknown, AnyAction>>();

storeSlice.ts

import { AppThunkAction } from './index';

export interface StandardReduxAction { type: 'STANDARD_REDUX' }
export interface ReduxThunkAction { type: 'REDUX_THUNK', data: unknown }

interface SliceNameActions {
    standardRedux: (show: boolean) => StandardReduxAction;
    reduxThunk: () => AppThunkAction<ReduxThunkAction>;
}

export const SliceNameActionCreators: SliceNameActions = {

    standardRedux: (): StandardReduxAction => { type: StandardReduxAction };

    reduxThunk: (): AppThunkAction<ReduxThunkAction> => async (dispatch, getState): Promise<void> => {
                    
        let response = await asyncCallSomewhere();
        dispatch({ type: ReduxThunkAction, data: response });
    }
}

anyComponent.tsx

import { useStoreDispatch } from 'store';
import { SliceNameActionCreators } from 'storeSlice';

const dispatch = useStoreDispatch();

const dispatchStandardRedux = () => dispatch(SliceNameActionCreators.standardRedux());
const dispatchReduxThunk = () => dispatch(SliceNameActionCreators.reduxThunk());

The currently recommended way of setting up React-Redux with typescript is using Redux Toolkit, a guide can be found here.

Menadione answered 9/12, 2021 at 5:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.