import type { GetThunkAPI } from '@reduxjs/toolkit'; import { createAsyncThunk } from '@reduxjs/toolkit'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports import { useDispatch, useSelector } from 'react-redux'; import type { AppDispatch, RootState } from './store'; export const useAppDispatch = useDispatch.withTypes(); export const useAppSelector = useSelector.withTypes(); export interface AsyncThunkRejectValue { skipAlert?: boolean; skipNotFound?: boolean; error?: unknown; } interface AppMeta { skipLoading?: boolean; } export const createAppAsyncThunk = createAsyncThunk.withTypes<{ state: RootState; dispatch: AppDispatch; rejectValue: AsyncThunkRejectValue; }>(); interface AppThunkConfig { state: RootState; dispatch: AppDispatch; rejectValue: AsyncThunkRejectValue; fulfilledMeta: AppMeta; rejectedMeta: AppMeta; } type AppThunkApi = Pick, 'getState' | 'dispatch'>; interface AppThunkOptions { skipLoading?: boolean; } const createBaseAsyncThunk = createAsyncThunk.withTypes(); export function createThunk( name: string, creator: (arg: Arg, api: AppThunkApi) => Returned | Promise, options: AppThunkOptions = {}, ) { return createBaseAsyncThunk( name, async ( arg: Arg, { getState, dispatch, fulfillWithValue, rejectWithValue }, ) => { try { const result = await creator(arg, { dispatch, getState }); return fulfillWithValue(result, { skipLoading: options.skipLoading, }); } catch (error) { return rejectWithValue({ error }, { skipLoading: true }); } }, { getPendingMeta() { if (options.skipLoading) return { skipLoading: true }; return {}; }, }, ); } const discardLoadDataInPayload = Symbol('discardLoadDataInPayload'); type DiscardLoadData = typeof discardLoadDataInPayload; type OnData = ( data: LoadDataResult, api: AppThunkApi & { actionArg: ActionArg; discardLoadData: DiscardLoadData; }, ) => ReturnedData | DiscardLoadData | Promise; type LoadData = ( args: Args, api: AppThunkApi, ) => Promise; type ArgsType = Record | undefined; // Overload when there is no `onData` method, the payload is the `onData` result export function createDataLoadingThunk( name: string, loadData: (args: Args) => Promise, thunkOptions?: AppThunkOptions, ): ReturnType>; // Overload when the `onData` method returns discardLoadDataInPayload, then the payload is empty export function createDataLoadingThunk( name: string, loadData: LoadData, onDataOrThunkOptions?: | AppThunkOptions | OnData, thunkOptions?: AppThunkOptions, ): ReturnType>; // Overload when the `onData` method returns nothing, then the mayload is the `onData` result export function createDataLoadingThunk( name: string, loadData: LoadData, onDataOrThunkOptions?: AppThunkOptions | OnData, thunkOptions?: AppThunkOptions, ): ReturnType>; // Overload when there is an `onData` method returning something export function createDataLoadingThunk< LoadDataResult, Args extends ArgsType, Returned, >( name: string, loadData: LoadData, onDataOrThunkOptions?: | AppThunkOptions | OnData, thunkOptions?: AppThunkOptions, ): ReturnType>; /** * This function creates a Redux Thunk that handles loading data asynchronously (usually from the API), dispatching `pending`, `fullfilled` and `rejected` actions. * * You can run a callback on the `onData` results to either dispatch side effects or modify the payload. * * It is a wrapper around RTK's [`createAsyncThunk`](https://redux-toolkit.js.org/api/createAsyncThunk) * @param name Prefix for the actions types * @param loadData Function that loads the data. It's (object) argument will become the thunk's argument * @param onDataOrThunkOptions * Callback called on the results from `loadData`. * * First argument will be the return from `loadData`. * * Second argument is an object with: `dispatch`, `getState` and `discardLoadData`. * It can return: * - `undefined` (or no explicit return), meaning that the `onData` results will be the payload * - `discardLoadData` to discard the `onData` results and return an empty payload * - anything else, which will be the payload * * You can also omit this parameter and pass `thunkOptions` directly * @param maybeThunkOptions * Additional Mastodon specific options for the thunk. Currently supports: * - `skipLoading` to avoid showing the loading bar when the request is in progress * @returns The created thunk */ export function createDataLoadingThunk< LoadDataResult, Args extends ArgsType, Returned, >( name: string, loadData: LoadData, onDataOrThunkOptions?: | AppThunkOptions | OnData, maybeThunkOptions?: AppThunkOptions, ) { let onData: OnData | undefined; let thunkOptions: AppThunkOptions | undefined; if (typeof onDataOrThunkOptions === 'function') onData = onDataOrThunkOptions; else if (typeof onDataOrThunkOptions === 'object') thunkOptions = onDataOrThunkOptions; if (maybeThunkOptions) { thunkOptions = maybeThunkOptions; } return createThunk( name, async (arg, { getState, dispatch }) => { const data = await loadData(arg, { dispatch, getState, }); if (!onData) return data as Returned; const result = await onData(data, { dispatch, getState, discardLoadData: discardLoadDataInPayload, actionArg: arg, }); // if there is no return in `onData`, we return the `onData` result if (typeof result === 'undefined') return data as Returned; // the user explicitely asked to discard the payload else if (result === discardLoadDataInPayload) return undefined as Returned; else return result; }, thunkOptions, ); }