React useDispatch hook as function to use then [duplicate]

I'm using redux-thunk to use async action creators. The result is also returned to the respective caller.

function fetchUserName(userId: number): Promise<string> {
  return Promise.resolve(`User ${userId}`)
}

function requestUserName(userId: number) {
  return (dispatch: Dispatch) => {
    return fetchUserName(userId).then(name => {
      dispatch({
        type: 'SET_USERNAME',
        payload: name,
      })
    })
  }
}

This way, the store is updated, while allowing the components to handle the response directly.

function User() {
  const dispatch = useDispatch()
  useEffect(() => {
    dispatch(requestUserName(1))
      .then(name => {
        console.log(`user name is ${name}`)
      })
      .catch(reason => {
        alert('failed fetching user name')
      })
  }, [])
}

This is working as intended, but it will not be compiled by TypeScript due to invalid types.

  1. The dispatch returned by useDispatch is not recognized as a function that returns a Promise and so TypeScript argues that Property 'then' does not exist on type '(dispatch: Dispatch<AnyAction>) => Promise<void>'..
  2. Even if it would be recognized so, the Promise should be correctly typed

How can this situation be solved?

It would be perfectly fine for me to create a wrapper around useDispatch or to redefine the type of dispatch but I have no idea how that type should look like in this particular situation.

Thank you very much for any suggestion.


Solution 1:

useDispatch returns the Dispatch type used by Redux, so you can only dispatch standard actions with it. To also dispatch thunk actions, declare its type as ThunkDispatch (from redux-thunk).

ThunkDispatch receives type parameters for the store state, extra thunk args and your action type. It allows to dispatch a ThunkAction, which basically is the inner function of requestUserName.

For example, you can type it like this:

import { ThunkDispatch } from "redux-thunk";
import { AnyAction } from "redux";

type State = { a: string }; // your state type
type AppDispatch = ThunkDispatch<State, any, AnyAction>; 
// or restrict to specific actions instead of AnyAction

function User() {
  const dispatch: AppDispatch = useDispatch();
  useEffect(() => {
    dispatch(requestUserName(1))
      .then(...)  // works now
  }, []);
  ...
}

AppDispatch can also be inferred from the store with typeof store.dispatch:

import thunk, { ThunkDispatch, ThunkMiddleware } from "redux-thunk";

const mw: ThunkMiddleware<State, AnyAction> = thunk;
const dummyReducer = (s: State | undefined, a: AnyAction) => ({} as State);
const store = createStore(dummyReducer, applyMiddleware(mw));

type AppDispatch = typeof store.dispatch // <-- get the type from store

TS Playground sample

See also redux's documentation on using typescript with hooks: https://redux.js.org/usage/usage-with-typescript#define-typed-hooks

Solution 2:

My common set up includes a typesafe AppDispatch and typed hooks;

import { createStore, applyMiddleware, PreloadedState, combineReducers } from 'redux';
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import thunk, { ThunkDispatch } from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension';

const middlewares = [thunk];
const enhancer = composeWithDevTools({ /* optional actionsBlacklist, etc */ });

const reducers = combineReducers({ /* ... */ })
export type RootState = ReturnType<typeof reducers>;

export const initStore = (initState?: PreloadedState<RootState>) =>
  createStore(reducers, initState, enhancer(applyMiddleware(...middlewares)));
export const store = initStore();

export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
type AppAction = ReturnType<typeof store.dispatch>;
export type AppDispatch = ThunkDispatch<RootState, any, AppAction>;
export const useAppDispatch = () => useDispatch<AppDispatch>();

Note it will be slightly different with Redux Toolkit as you configureStore instead combineReducers