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!
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.