Redux-toolkit: Synchronous alternative to createAsyncThunk

Created on 23 Mar 2021  路  7Comments  路  Source: reduxjs/redux-toolkit

Hi, I need to access getState and dispatch conditionally, the best way for my use case seems to be using a thunk.
I wanted to know if there's an alternative to createAsyncThunk that doesn't assume asynchronicity and doesn't return a Promise, since our code is all synchronous.

Thank you!

All 7 comments

Per the docs, the point of createAsyncThunk is to abstract the process of dispatching pending/fulfilled/rejected actions to describe the lifecycle of an async request / promise.

To do synchronous work, you just need to write a thunk by hand as usual:

const doSomeSyncWork = (id) => (dispatch, getState) => {
  // do something with the id, dispatch, and getState here
}

// later
dispatch(doSomeSyncWork(123))

See https://redux.js.org/tutorials/fundamentals/part-6-async-logic#redux-middleware-and-side-effects and https://gist.github.com/markerikson/ea4d0a6ce56ee479fe8b356e099f857e for more examples.

Thank you, I didn't know actions had access to getState by default.

In case it helps anyone in the future, here's what I ended up with:

import { useDispatch } from "react-redux";

import type { AppDispatch, CreateStore } from "./store";

type GetState = ReturnType<CreateStore>["getState"];

type SyncThunk = (payload: any, dispatch: AppDispatch, getState: GetState) => void;

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

export const createSyncThunk = (thunk: SyncThunk) => (payload?: any) => (dispatch: AppDispatch, getState: GetState) =>
  thunk(payload, dispatch, getState);

FWIW, that will work for you, but conceptually that's a bit different than how thunks actually work in that they don't have (payload, dispatch, getState) as arguments - just (dispatch, getState), and whatever arguments to the action creator are captured via closure.

Thank you for the observation!

So, this way will be more conceptually correct, right?

import { useDispatch } from "react-redux";

import type { AppDispatch, CreateStore } from "./store";

type GetState = ReturnType<CreateStore>["getState"];

type ThunkCreator = (...args: any) => (dispatch: AppDispatch, getState: GetState) => void;

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

export const createSyncThunk = (thunkCreator: ThunkCreator) => (...args: any) => (
  dispatch: AppDispatch,
  getState: GetState
) => thunkCreator(...args)(dispatch, getState);

There really isn't much of a point to a createSyncThunk, tbh. There's really nothing that needs to be abstracted.

We do show how to write an AppThunk type in the main Redux docs:

https://redux.js.org/recipes/usage-with-typescript#type-checking-redux-thunks

and then the usage just needs to be:

const myThunkActionCreator = (id: string) : AppThunk = (dispatch, getState) => {
  // write code with id, dispatch, and getState here
}

The question here is really why you spend the energy to build this abstraction instead of just writing the thunk.

What do you gain by doing

const thunk = createSyncThunk((arg) => (dispatch, getState) => { /* code */ }))

instead of

const thunk = (arg) => (dispatch: AppDispatch, getState: GetState) => { /* code */ })

?

I mean, yes, you save the dispatch: AppDispatch, getState: GetState but your variant casts the input args to any[] and omits the return type.

If you feel you need the abstraction, try something like

export const createSyncThunk = <Args extends any[], R extends any>(thunk: (...args: Args) => (dispatch: AppDispatch, getState: GetState) => R ) => thunk;

That could be used like

const thunk = createSyncThunk((arg1: string, arg2: number) => (dispatch, getState) => { return "foo" }))

and all types would be correctly inferred.

Perfect, yes, it's to avoid declaring types on every sync thunk we create in our app since we'll have a lot of them (it's a graphical editor and has a lot of complex synchronous logic). Thanks for pointing out the issues in the args and return types.

Was this page helpful?
0 / 5 - 0 ratings