redux-toolkit icon indicating copy to clipboard operation
redux-toolkit copied to clipboard

Race condition with optimistic updates and polling/refetch on focus

Open agusterodin opened this issue 1 month ago • 2 comments

I'm experiencing an issue where if I apply an optimistic update when performing a mutation (slow POST endpoint) and a polling refetch (on a fast GET endpoint) occurs after the optimistic update but while the mutation is still in process, the optimistic update gets lost and the UI flickers back and forth.

Quick polling intervals (eg: 100) make this really easy to recreate.

The same issue (optimistic update gets lost) occurs on "refetch on focus" as well.


Ideas on possible solutions (none of which have worked):

  1. Wrap the query hook at a global level and use skip if mutation is in progress. Unfortunately this doesn't work. The optimistic update doesn't become visible in any part of the application using the hook when skip is true.
export const getCampaignsApi = (state: State) => state.campaignsApi

export const getMutationInProgress = createSelector([getCampaignsApi], campaignsApi => {
  return Object.values(campaignsApi.mutations).some(mutation => mutation?.status === 'pending')
})

export function useGetAllCampaignMetadataQuery(options?: UseQueryOptions) {
 const mutationInProgress = useSelector(getMutationInProgress)
 return campaignApiSlice.useGetAllCampaignMetadataQuery(undefined, {
   skip: mutationInProgress,
   ...options
 })
}
  1. Try using the merge functionality. Unfortunately an error is thrown if I try to use store.getState() it within the merge function. getState is not exposed in the merge callback so I would have no other way of accessing the current state.
export function getMutationInProgress() { {
  const state = store.getState()
  return Object.values(campaignsApi.mutations).some(mutation => mutation?.status === 'pending')
}

getAllCampaigns: builder.query({
  // other parts of endpoint definition omitted
  merge: (currentCacheData, responseData) => {
    const mutationInProgress = getMutationInProgress()
    if (mutationInProgress) {
      return currentCacheData
    }
    else {
      return responseData
    }
})

Video Screenshots

Polling https://github.com/user-attachments/assets/7627e4c4-3460-480c-9db8-0c6003a96bcf

Refetch on focus https://github.com/user-attachments/assets/cd9557ad-5559-464b-82ce-4640673c9fbc


Minimal reproduction

Unfortunately i'm not able to set up a fully working example due to the fact that a simple mock server that serves the same mock response every time wouldn't be able to save the changes of a mutation (since I would need a proper database to do so). The real endpoints i'm using on my project require authentication. That being said, here is the stripped down code I used for my video example:

https://github.com/agusterodin/rtkq-playground/tree/optimistic-update-polling-race-condition

To run this use pnpm i and pnpm dev. I have the polling and refetch on focus lines commented out but had them uncommented when taking their respective video screenshots.

agusterodin avatar Nov 22 '25 04:11 agusterodin

Yeah, amazing how much timing issues are a problem around this type of work :)

Was planning to spend some time on issues tomorrow - I'll take a look!

markerikson avatar Nov 22 '25 04:11 markerikson

Spent some time working on this. Made a bunch of local changes around things like tracking "data version" in the cache, thought I had something working.

Now I'm actually a bit confused and want to take a step back :)

Can you talk me through both the actual behavior you're seeing, and what you would expect or want the behavior to be instead? Like, the sequence of events and state changes?

Would you expect the polling refetch to be ignored completely and not update the state?

markerikson avatar Nov 22 '25 19:11 markerikson

Current behavior:

  • I trigger a mutation and apply an optimistic update on "get all campaign metadata" endpoint.
  • Polling causes a refetch of "get all campaign metadata" (very fast endpoint) before my mutation completes (extremely slow POST endpoint).
  • The optimistic update I made is lost due to the refetch (old value is redisplayed).
  • The mutation (slow POST endpoint) finally finishes and cache invalidation (via provides/invalidates tags) causes a refetch of "get all campaign metadata".
  • Up-to-date value is shown once refetch triggered by cache invalidation finishes (new value is redisplayed again).

Expected behavior:

  • I trigger a mutation and apply an optimistic update on "get all campaign metadata" endpoint.
  • Polling causes a refetch of "get all campaign metadata" (very fast endpoint) before my mutation completes (extremely slow POST endpoint).
  • The optimistic update is somehow preserved while the mutation continues to run, regardless of additional refetches that occur in the process.

I agree that I wouldn't expect the fresh data to be completely ignored. Just spitballing, but maybe the fresh data could be used, but with patches (if any) from any in-flight mutations re-applied to it.

agusterodin avatar Dec 17 '25 05:12 agusterodin