Repository: reduxjs/redux-toolkit (ts)

Issue:
Possibility of customising function that generates requestId in the action's meta of createAsyncThunk
Hi! :) First of all, let me express my gratitude that this library (toolkit) had already existed back in February/March when I first re-architected some project at my current company – it's been making my life so much easier since then, and fun to use – so a big THANK YOU! 🙏😇🙇🏻‍♂️🙇🏻‍♂️

Recently though, there is one aspect/requirement that I have to fulfil in my app, which is all the network requests have to be able to be tracked by **[uuidv4](https://www.npmjs.com/package/uuid#uuidv4options-buffer-offset)** (to conform with the existing logging/analysis infrastructure).

As I want to make use of the RTK built-in feature as much as possible, would it be possible to add a possibility to pass a custom function other than `nanoid`  (in my case, it'd be `uuidv4`) to generate the `action.meta.requestId` used by `createAsyncThunk`?

This is a rather simplistic construct of how my slice would look like:
```javascript
extraReducers: {
  [fetchSomething.pending]: (state, action) => {
    state.loading = true;
    state.pendingRequestIds.push(action.meta.requestId);
  },
},
```

I mean, currently, to satisfy this requirement, I'm making use of `action.meta.arg.requestId` and doing the wiring when dispatching: `dispatch(fetchSomething({ requestId: uuidv4() }))`. So I thought it would be nice and cleaner if this feature could be implemented per se.

Thank you.

Pull request description:
Add `idGenerator` option to `createAsyncThunk` (#743)
Closes #743 

This PR adds an optional `idGenerator` option to the `createAsyncThunk` options to allow specifying how request IDs are generated rather than using nanoid.

Patch:
diff --git a/docs/api/createAsyncThunk.mdx b/docs/api/createAsyncThunk.mdx
index 16b23f6a..e4c8a34a 100644
--- a/docs/api/createAsyncThunk.mdx
+++ b/docs/api/createAsyncThunk.mdx
@@ -89,6 +89,7 @@ An object with the following optional fields:
 
 - `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.
 - `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`.
+- `idGenerator`: a function to use when generating the `requestId` for the request sequence. Defaults to use [nanoid](./otherExports.mdx/#nanoid).
 
 ## Return Value
 
diff --git a/docs/api/otherExports.mdx b/docs/api/otherExports.mdx
index c01fecfc..411066d1 100644
--- a/docs/api/otherExports.mdx
+++ b/docs/api/otherExports.mdx
@@ -11,7 +11,7 @@ Redux Toolkit exports some of its internal utilities, and re-exports additional
 
 ### `nanoid`
 
-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.
+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.
 
 ```ts
 import { nanoid } from '@reduxjs/toolkit'
diff --git a/etc/redux-toolkit.api.md b/etc/redux-toolkit.api.md
index e74a1a5c..3eb94603 100644
--- a/etc/redux-toolkit.api.md
+++ b/etc/redux-toolkit.api.md
@@ -92,6 +92,7 @@ export type AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig extends AsyncThu
 export interface AsyncThunkOptions<ThunkArg = void, ThunkApiConfig extends AsyncThunkConfig = {}> {
     condition?(arg: ThunkArg, api: Pick<GetThunkAPI<ThunkApiConfig>, 'getState' | 'extra'>): boolean | undefined;
     dispatchConditionRejection?: boolean;
+    idGenerator?: () => string;
     // (undocumented)
     serializeError?: (x: unknown) => GetSerializedErrorType<ThunkApiConfig>;
 }
diff --git a/src/createAsyncThunk.ts b/src/createAsyncThunk.ts
index f9e70ac7..8b2b5882 100644
--- a/src/createAsyncThunk.ts
+++ b/src/createAsyncThunk.ts
@@ -223,6 +223,13 @@ export interface AsyncThunkOptions<
   dispatchConditionRejection?: boolean
 
   serializeError?: (x: unknown) => GetSerializedErrorType<ThunkApiConfig>
+
+  /**
+   * A function to use when generating the `requestId` for the request sequence.
+   *
+   * @default `nanoid`
+   */
+  idGenerator?: () => string
 }
 
 export type AsyncThunkPendingActionCreator<
@@ -392,7 +399,7 @@ If you want to use the AbortController to react to \`abort\` events, please cons
     arg: ThunkArg
   ): AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig> {
     return (dispatch, getState, extra) => {
-      const requestId = nanoid()
+      const requestId = (options?.idGenerator ?? nanoid)()
 
       const abortController = new AC()
       let abortReason: string | undefined


Test patch:
diff --git a/src/createAsyncThunk.test.ts b/src/createAsyncThunk.test.ts
index f8d7343a..dfe9d458 100644
--- a/src/createAsyncThunk.test.ts
+++ b/src/createAsyncThunk.test.ts
@@ -680,3 +680,65 @@ describe('unwrapResult', () => {
     await expect(unwrapPromise2.unwrap()).rejects.toBe('rejectWithValue!')
   })
 })
+
+describe('idGenerator option', () => {
+  const getState = () => ({})
+  const dispatch = (x: any) => x
+  const extra = {}
+
+  test('idGenerator implementation - can customizes how request IDs are generated', async () => {
+    function makeFakeIdGenerator() {
+      let id = 0
+      return jest.fn(() => {
+        id++
+        return `fake-random-id-${id}`
+      })
+    }
+
+    let generatedRequestId = ''
+
+    const idGenerator = makeFakeIdGenerator()
+    const asyncThunk = createAsyncThunk(
+      'test',
+      async (args: void, { requestId }) => {
+        generatedRequestId = requestId
+      },
+      { idGenerator }
+    )
+
+    // dispatching the thunks should be using the custom id generator
+    const promise0 = asyncThunk()(dispatch, getState, extra)
+    expect(generatedRequestId).toEqual('fake-random-id-1')
+    expect(promise0.requestId).toEqual('fake-random-id-1')
+    expect((await promise0).meta.requestId).toEqual('fake-random-id-1')
+
+    const promise1 = asyncThunk()(dispatch, getState, extra)
+    expect(generatedRequestId).toEqual('fake-random-id-2')
+    expect(promise1.requestId).toEqual('fake-random-id-2')
+    expect((await promise1).meta.requestId).toEqual('fake-random-id-2')
+
+    const promise2 = asyncThunk()(dispatch, getState, extra)
+    expect(generatedRequestId).toEqual('fake-random-id-3')
+    expect(promise2.requestId).toEqual('fake-random-id-3')
+    expect((await promise2).meta.requestId).toEqual('fake-random-id-3')
+
+    generatedRequestId = ''
+    const defaultAsyncThunk = createAsyncThunk(
+      'test',
+      async (args: void, { requestId }) => {
+        generatedRequestId = requestId
+      }
+    )
+    // dispatching the default options thunk should still generate an id,
+    // but not using the custom id generator
+    const promise3 = defaultAsyncThunk()(dispatch, getState, extra)
+    expect(generatedRequestId).toEqual(promise3.requestId)
+    expect(promise3.requestId).not.toEqual('')
+    expect(promise3.requestId).not.toEqual(
+      expect.stringContaining('fake-random-id')
+    )
+    expect((await promise3).meta.requestId).not.toEqual(
+      expect.stringContaining('fake-fandom-id')
+    )
+  })
+})
diff --git a/type-tests/files/createAsyncThunk.typetest.ts b/type-tests/files/createAsyncThunk.typetest.ts
index 890fa74d..57789b22 100644
--- a/type-tests/files/createAsyncThunk.typetest.ts
+++ b/type-tests/files/createAsyncThunk.typetest.ts
@@ -393,3 +393,27 @@ const anyAction = { type: 'foo' } as AnyAction
     expectType<Funky>(anyAction.error)
   }
 }
+
+/**
+ * `idGenerator` option takes no arguments, and returns a string
+ */
+{
+  const returnsNumWithArgs = (foo: any) => 100
+  // has to stay on one line or type tests fail in older TS versions
+  // prettier-ignore
+  // @ts-expect-error
+  const shouldFailNumWithArgs = createAsyncThunk('foo', () => {}, { idGenerator: returnsNumWithArgs })
+
+  const returnsNumWithoutArgs = () => 100
+  // prettier-ignore
+  // @ts-expect-error
+  const shouldFailNumWithoutArgs = createAsyncThunk('foo', () => {}, { idGenerator: returnsNumWithoutArgs })
+
+  const returnsStrWithArgs = (foo: any) => 'foo'
+  // prettier-ignore
+  // @ts-expect-error
+  const shouldFailStrArgs = createAsyncThunk('foo', () => {}, { idGenerator: returnsStrWithArgs })
+
+  const returnsStrWithoutArgs = () => 'foo'
+  const shouldSucceed = createAsyncThunk('foo', () => {}, { idGenerator: returnsStrWithoutArgs })
+}