Skip to content

Commit 309c4bd

Browse files
authored
Add idGenerator option to createAsyncThunk (#743) (#976)
1 parent 4701e89 commit 309c4bd

File tree

6 files changed

+97
-2
lines changed

6 files changed

+97
-2
lines changed

docs/api/createAsyncThunk.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ An object with the following optional fields:
8989

9090
- `condition`: a callback that can be used to skip execution of the payload creator and all action dispatches, if desired. See [Canceling Before Execution](#canceling-before-execution) for a complete description.
9191
- `dispatchConditionRejection`: if `condition()` returns `false`, the default behavior is that no actions will be dispatched at all. If you still want a "rejected" action to be dispatched when the thunk was canceled, set this flag to `true`.
92+
- `idGenerator`: a function to use when generating the `requestId` for the request sequence. Defaults to use [nanoid](./otherExports.mdx/#nanoid).
9293

9394
## Return Value
9495

docs/api/otherExports.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Redux Toolkit exports some of its internal utilities, and re-exports additional
1111

1212
### `nanoid`
1313

14-
An inlined copy of [`nanoid/nonsecure`](https://github.com/ai/nanoid). Generates a non-cryptographically-secure random ID string. Automatically used by `createAsyncThunk` for request IDs, but may also be useful for other cases as well.
14+
An inlined copy of [`nanoid/nonsecure`](https://github.com/ai/nanoid). Generates a non-cryptographically-secure random ID string. `createAsyncThunk` uses this by default for request IDs. May also be useful for other cases as well.
1515

1616
```ts
1717
import { nanoid } from '@reduxjs/toolkit'

etc/redux-toolkit.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export type AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig extends AsyncThu
9292
export interface AsyncThunkOptions<ThunkArg = void, ThunkApiConfig extends AsyncThunkConfig = {}> {
9393
condition?(arg: ThunkArg, api: Pick<GetThunkAPI<ThunkApiConfig>, 'getState' | 'extra'>): boolean | undefined;
9494
dispatchConditionRejection?: boolean;
95+
idGenerator?: () => string;
9596
// (undocumented)
9697
serializeError?: (x: unknown) => GetSerializedErrorType<ThunkApiConfig>;
9798
}

src/createAsyncThunk.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,3 +680,65 @@ describe('unwrapResult', () => {
680680
await expect(unwrapPromise2.unwrap()).rejects.toBe('rejectWithValue!')
681681
})
682682
})
683+
684+
describe('idGenerator option', () => {
685+
const getState = () => ({})
686+
const dispatch = (x: any) => x
687+
const extra = {}
688+
689+
test('idGenerator implementation - can customizes how request IDs are generated', async () => {
690+
function makeFakeIdGenerator() {
691+
let id = 0
692+
return jest.fn(() => {
693+
id++
694+
return `fake-random-id-${id}`
695+
})
696+
}
697+
698+
let generatedRequestId = ''
699+
700+
const idGenerator = makeFakeIdGenerator()
701+
const asyncThunk = createAsyncThunk(
702+
'test',
703+
async (args: void, { requestId }) => {
704+
generatedRequestId = requestId
705+
},
706+
{ idGenerator }
707+
)
708+
709+
// dispatching the thunks should be using the custom id generator
710+
const promise0 = asyncThunk()(dispatch, getState, extra)
711+
expect(generatedRequestId).toEqual('fake-random-id-1')
712+
expect(promise0.requestId).toEqual('fake-random-id-1')
713+
expect((await promise0).meta.requestId).toEqual('fake-random-id-1')
714+
715+
const promise1 = asyncThunk()(dispatch, getState, extra)
716+
expect(generatedRequestId).toEqual('fake-random-id-2')
717+
expect(promise1.requestId).toEqual('fake-random-id-2')
718+
expect((await promise1).meta.requestId).toEqual('fake-random-id-2')
719+
720+
const promise2 = asyncThunk()(dispatch, getState, extra)
721+
expect(generatedRequestId).toEqual('fake-random-id-3')
722+
expect(promise2.requestId).toEqual('fake-random-id-3')
723+
expect((await promise2).meta.requestId).toEqual('fake-random-id-3')
724+
725+
generatedRequestId = ''
726+
const defaultAsyncThunk = createAsyncThunk(
727+
'test',
728+
async (args: void, { requestId }) => {
729+
generatedRequestId = requestId
730+
}
731+
)
732+
// dispatching the default options thunk should still generate an id,
733+
// but not using the custom id generator
734+
const promise3 = defaultAsyncThunk()(dispatch, getState, extra)
735+
expect(generatedRequestId).toEqual(promise3.requestId)
736+
expect(promise3.requestId).not.toEqual('')
737+
expect(promise3.requestId).not.toEqual(
738+
expect.stringContaining('fake-random-id')
739+
)
740+
expect((await promise3).meta.requestId).not.toEqual(
741+
expect.stringContaining('fake-fandom-id')
742+
)
743+
})
744+
})

src/createAsyncThunk.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,13 @@ export interface AsyncThunkOptions<
223223
dispatchConditionRejection?: boolean
224224

225225
serializeError?: (x: unknown) => GetSerializedErrorType<ThunkApiConfig>
226+
227+
/**
228+
* A function to use when generating the `requestId` for the request sequence.
229+
*
230+
* @default `nanoid`
231+
*/
232+
idGenerator?: () => string
226233
}
227234

228235
export type AsyncThunkPendingActionCreator<
@@ -392,7 +399,7 @@ If you want to use the AbortController to react to \`abort\` events, please cons
392399
arg: ThunkArg
393400
): AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig> {
394401
return (dispatch, getState, extra) => {
395-
const requestId = nanoid()
402+
const requestId = (options?.idGenerator ?? nanoid)()
396403

397404
const abortController = new AC()
398405
let abortReason: string | undefined

type-tests/files/createAsyncThunk.typetest.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,3 +393,27 @@ const anyAction = { type: 'foo' } as AnyAction
393393
expectType<Funky>(anyAction.error)
394394
}
395395
}
396+
397+
/**
398+
* `idGenerator` option takes no arguments, and returns a string
399+
*/
400+
{
401+
const returnsNumWithArgs = (foo: any) => 100
402+
// has to stay on one line or type tests fail in older TS versions
403+
// prettier-ignore
404+
// @ts-expect-error
405+
const shouldFailNumWithArgs = createAsyncThunk('foo', () => {}, { idGenerator: returnsNumWithArgs })
406+
407+
const returnsNumWithoutArgs = () => 100
408+
// prettier-ignore
409+
// @ts-expect-error
410+
const shouldFailNumWithoutArgs = createAsyncThunk('foo', () => {}, { idGenerator: returnsNumWithoutArgs })
411+
412+
const returnsStrWithArgs = (foo: any) => 'foo'
413+
// prettier-ignore
414+
// @ts-expect-error
415+
const shouldFailStrArgs = createAsyncThunk('foo', () => {}, { idGenerator: returnsStrWithArgs })
416+
417+
const returnsStrWithoutArgs = () => 'foo'
418+
const shouldSucceed = createAsyncThunk('foo', () => {}, { idGenerator: returnsStrWithoutArgs })
419+
}

0 commit comments

Comments
 (0)