Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

type-safe useDispatch with redux-thunk

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.

like image 1000
K. D. Avatar asked Jan 18 '20 13:01

K. D.


2 Answers

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

like image 98
ford04 Avatar answered Sep 20 '22 04:09

ford04


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

like image 41
Mikel Avatar answered Sep 22 '22 04:09

Mikel