Search code examples
reactjstypescriptreact-reduxredux-toolkitredux-promise-middleware

How should I type React's dispatch function to be able to do a "then" method like a Promise


I have a simple app which dispatches an action on first load to populate the store. I want to be able to run a then method on dispatch but typescript complains about this.

(According to redux's documentation, the return value of dispatching an action is the return value of the action itself)

Code available in Codesandbox

// app.jsx
function App() {
  const dispatch = useAppDispatch();

  useEffect(() => {
    dispatch(getTodos()).then((todos) => console.log(todos));
    //                   ^^^^^
    //                   Property 'then' does not exist on type '{ type: "GET_TODOS"; payload: Promise<AxiosResponse<Todo[], any>>; }'.
  }, []);
  return <div className="App">Hello World!</div>;
}

Store Configuration

I use @reduxjs/toolkit to configure my store and have a redux-promise-middleware set up for it so that upon "fulfillment" of my promised-based actions an <ACTION>_FULFILLED action will also be dispatched.

// store.ts
import { configureStore } from '@reduxjs/toolkit';
import promiseMiddleware from 'redux-promise-middleware';
import rootReducer from './reducer';
import { useDispatch } from 'react-redux';

const store = configureStore({
    reducer: rootReducer,
    middleware: [promiseMiddleware],
});

export type RootState = ReturnType<typeof rootReducer>;
export type AppDispatch = typeof store.dispatch;

export const useAppDispatch = () => useDispatch<AppDispatch>();

export default store;

Reducer

// reducer.ts
import produce, { Draft } from "immer";
import { Action } from "./action";

export type Todo = {
    userId: number;
    id: number;
    title: string;
    completed: boolean;
}

interface State {
  todos: Todo[];
}
const initialState: State = {
  todos: []
};

const reducer = produce((draft: Draft<State>, action: Action) => {
  switch (action.type) {
    case "GET_TODOS_FULFILLED": {
      const todos = action.payload.data;
      return todos;
    }
  }
}, initialState);

export default reducer;

Action

// action.ts
import axios from "axios";
import type { AxiosResponse } from "axios";
import type { Todo } from './reducer'

export const getTodos = (): {
  type: "GET_TODOS";
  payload: Promise<AxiosResponse<Todo[]>>;
} => ({
  type: "GET_TODOS",
  payload: axios.get("https://jsonplaceholder.typicode.com/todos")
});

export type Action = ReturnType<typeof getTodos>;

Solution

  • Ok I figured it out by myself. The problem was the incorrect typing of useAppDispatch.

    So in store.ts instead of having

    export const useAppDispatch = () => useDispatch<AppDispatch>();
    

    We should have

    import type { ThunkAction, ThunkDispatch } from "redux-thunk";
    export type AppThunk<RT = void> = ThunkAction<
      Promise<RT>,
      RootState,
      unknown,
      AnyAction
    >;
    export type AppThunkDispatch = ThunkDispatch<RootState, void, Action>;
    export const useAppDispatch = () => useDispatch<AppThunkDispatch>();
    

    And then in action.ts we could write our thunk actions like this:

    export const getTodos =
      (): AppThunk<{ type: "SET_TODOS"; payload: Todo[] }> => (dispatch) =>
        axios
          .get<Todo[]>("https://jsonplaceholder.typicode.com/todos")
          .then((response) => response.data)
          .then((todos) => dispatch(setTodos(todos)));
    
    export const setTodos = (
      todos: Todo[]
    ): {
      type: "SET_TODOS";
      payload: Todo[];
    } => ({
      type: "SET_TODOS",
      payload: todos
    });
    

    And lastly for using dispatch anywhere in our app:

    dispatch(getTodos()).then((todos) => console.log(todos));