Repository: reduxjs/redux-toolkit

Problem statement:
onCacheEntryAdded doesn't work with fixedCacheKey
**onCacheEntryAdded** is not called when **fixedCacheKey** is specified for mutation ([getCacheKey doesn't handle that case](https://github.com/reduxjs/redux-toolkit/blob/f7a828255e0612bed0856a0b6d1b9b8d6d6ac30d/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts#L254))

```
const [fetchList, list] = rtkApi.useListMutation({
    fixedCacheKey: 'FIXED_CACHE_KEY',
});

...

list: builder.mutation({
  query: () => ({
    method: "POST",
    url: `/some-url`,
  }),
  async onCacheEntryAdded(arg, { cacheDataLoaded }) {
    console.log("log me please");
    await cacheDataLoaded;
    console.log("log again");
  })
})
```



Hints:
I have an issue too with **fixedCacheKey** when using it in hooks, it does not seem to have any effect (using the same hook with the same fixedCacheKey and then fetching with the same set of parameters still runs an Ajax query against the server every time)
@Cadrach as a mutation is meant to *change something on the server*, it will always be executed then you call that `trigger` function. What `fixedCacheKey` does is that it allows you to tap into the result of another mutation made somewhere else. But if you call `trigger`, yes, it will run again. That is the point and that's why we call the function `trigger` in the docs.

Generally, that has nothing to do with the issue discussed here though - if you think you found a bug, please open a separate issue in the future.
it is mentioned in the issue that `getCacheKey()` doesn't handle any fixedCacheKey functionality, which I think is indeed the core issue.

Broken Flow
---

1. fixedCacheKey creates a mutation with the fixedCacheKey as the cacheKey in the mutations state

2. `buildCacheLifecycleHandler` correctly identifies the request as a mutationThunk

3. `getCacheKey()` returns the requestId as the cacheKey in the handler because it is a mutationThunk

```ts
  function getCacheKey(action: any) {
    if (isQueryThunk(action)) return action.meta.arg.queryCacheKey
    if (isMutationThunk(action)) return action.meta.requestId //Line to change
```

4. conditional directly below that adds entry to the cache searches for the requestId as the key in mutations state instead of searching for the fixedKey

```ts
} else if (mutationThunk.pending.match(action)) {
      const state = mwApi.getState()[reducerPath].mutations[cacheKey] //Only searches for requestId currently (always undefined)
      if (state) {
        handleNewKey(
        ...
        )
      }
```
---

Solution?

I think just changing step 3 code to instead be the following code should fix it but I am not sure if I am overseeing something as I am not a cache lifecycle expert so I am not entirely sure what the expected behaviour beyond this is, but I can only imagine fixedCacheKey using the cache is at least a worthy starting point.

```ts
  function getCacheKey(action: any) {
    if (isQueryThunk(action)) return action.meta.arg.queryCacheKey
    if (isMutationThunk(action)) {
      // Use fixedCacheKey if available, otherwise fall back to requestId
      return action.meta.arg.fixedCacheKey ?? action.meta.requestId;
    }
```
    
I'll do a PR with a test added to fixedCacheKey tests to snapshot the cache on the off chance I am not having some major oversight


Patch:
diff --git a/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts b/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts
--- a/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts
+++ b/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts
@@ -253,7 +253,9 @@ export const buildCacheLifecycleHandler: InternalHandlerBuilder = ({
 
   function getCacheKey(action: any) {
     if (isQueryThunk(action)) return action.meta.arg.queryCacheKey
-    if (isMutationThunk(action)) return action.meta.requestId
+    if (isMutationThunk(action)) {
+      return action.meta.arg.fixedCacheKey ?? action.meta.requestId
+    }
     if (api.internalActions.removeQueryResult.match(action))
       return action.payload.queryCacheKey
     if (api.internalActions.removeMutationResult.match(action))


Test patch:
diff --git a/packages/toolkit/src/query/tests/useMutation-fixedCacheKey.test.tsx b/packages/toolkit/src/query/tests/useMutation-fixedCacheKey.test.tsx
--- a/packages/toolkit/src/query/tests/useMutation-fixedCacheKey.test.tsx
+++ b/packages/toolkit/src/query/tests/useMutation-fixedCacheKey.test.tsx
@@ -8,8 +8,11 @@ import {
   waitFor,
   act,
 } from '@testing-library/react'
+import { vi } from 'vitest'
 
 describe('fixedCacheKey', () => {
+  const onNewCacheEntry = vi.fn()
+
   const api = createApi({
     async baseQuery(arg: string | Promise<string>) {
       return { data: await arg }
@@ -354,4 +357,38 @@ describe('fixedCacheKey', () => {
     expect(getByTestId(c1, 'status').textContent).toBe('fulfilled')
     expect(getByTestId(c1, 'data').textContent).toBe('this should be visible')
   })
+
+  test('using fixedCacheKey should create a new cache entry', async () => {
+    api.enhanceEndpoints({
+      endpoints: {
+        send: {
+          onCacheEntryAdded: (arg) => onNewCacheEntry(arg),
+        },
+      },
+    })
+
+    render(<Component name="C1" fixedCacheKey={'testKey'} />, {
+      wrapper: storeRef.wrapper,
+    })
+
+    let c1 = screen.getByTestId('C1')
+
+    expect(getByTestId(c1, 'status').textContent).toBe('uninitialized')
+    expect(getByTestId(c1, 'originalArgs').textContent).toBe('undefined')
+
+    await act(async () => {
+      getByTestId(c1, 'trigger').click()
+      await Promise.resolve()
+    })
+
+    expect(onNewCacheEntry).toHaveBeenCalledWith('C1')
+
+    api.enhanceEndpoints({
+      endpoints: {
+        send: {
+          onCacheEntryAdded: undefined,
+        },
+      },
+    })
+  })
 })