import { AsyncThunkPayloadCreator, Dispatch, createAsyncThunk, } from '@reduxjs/toolkit' import { ThunkAPI } from './redux.types' type DebounceOptionsType = { /** * The number of milliseconds to delay * @defaultValue `300` */ wait: number /** * The maximum time `payloadCreator` is allowed to be delayed before * it's invoked. * @defaultValue `0` */ maxWait?: number /** * Specify invoking on the leading edge of the timeout. * @defaultValue `false` */ leading?: boolean } type AsyncThunkConfig = ThunkAPI & { dispatch?: Dispatch rejectValue?: unknown serializedErrorType?: unknown pendingMeta?: unknown fulfilledMeta?: unknown rejectedMeta?: unknown } /** * A debounced analogue of the `createAsyncThunk` from `@reduxjs/toolkit` * @param typePrefix - a string action type value * @param payloadCreator - a callback function that should return a promise containing the result of some asynchronous logic * @param debounceOptions - the debounce options object */ const createDebouncedAsyncThunk = < Returned, ThunkArg, ThunkApiConfig extends AsyncThunkConfig, >( typePrefix: string, payloadCreator: AsyncThunkPayloadCreator, debounceOptions?: DebounceOptionsType, ) => { const { wait = 300, maxWait = 0, leading = false } = debounceOptions ?? {} let debounceTimer: ReturnType | null = null let maxWaitTimer: ReturnType | null | undefined = null let resolve: ((_value: boolean) => void) | undefined const cancel = (): void => { if (resolve) { resolve(false) resolve = undefined } } const invoke = (): void => { if (maxWaitTimer) clearTimeout(maxWaitTimer) maxWaitTimer = undefined if (resolve) { resolve(true) resolve = undefined } } const debounceExecutionCondition = (): Promise | boolean => { const immediate = leading && !debounceTimer // Start debounced condition resolution if (debounceTimer) clearTimeout(debounceTimer) debounceTimer = setTimeout(() => { invoke() debounceTimer = null }, wait) if (immediate) { return true } cancel() // Start max wait condition resolution if (maxWait && !maxWaitTimer) { maxWaitTimer = setTimeout(invoke, maxWait) } return new Promise((res) => { resolve = res }) } return createAsyncThunk(typePrefix, payloadCreator, { condition: debounceExecutionCondition, }) } export default createDebouncedAsyncThunk