Search code examples
javascriptreactjsreduxredux-toolkitrtk-query

Optimistic updates in React Tool Kit


I have a list of Items, loaded by a getItems query and each item has a button that can be toggled between selected and unselected. I also have UI to create a new item to add to the list.

When a button is toggled on an item, I fire off an updateItem mutation and optimistically update that item in the getItems cache, so that the button toggles immediately. Behind the scenes, RTK makes the updateItem mutation, and when it succeeds and invalidates the tag for that item, it makes a second getItems query to get the latest data for the items. This will then pick up the remote change made earlier and overwrite the optimistic update. This works fine, and the UI feels quick and responsive.

However, I also need to be able to create new items. RTK Query doesn't support optimistic updates for new items, so I need to show a loading spinner in front of the list of Items and prevent any further interaction with it until the query returns, invalidates the getItems query, and (re)loads them, this time including the newly created item in the returned items.

How can I have the UI behave differently in these two cases:

  • If there was an optimistic update as a result of updateItem we have the UI in the correct state so don't show a spinner or lock the UI during the refetch of getItems
  • If there as a createItem mutation, lock the UI and show a spinner during the refetch of getItems

The problem here is that although I can know that the request isFetching, when getItems is refetched I have no way of differentiating between why it was refetched and no way to know if there was an optimistic update or not. This means I have no way to decide whether to show the spinner and lock the UI.


Solution

  • Instead of depending on the isFetching, you can directly listen for the createItem mutation using a listener middleware. Here is a sample implementation:

    export const itemListenerMiddleware = createListenerMiddleware()
    
    itemListenerMiddleware.startListening({
      matcher: isAnyOf(
        itemApi.endpoints.createItem.matchPending, // mutation has started
        // add additional matchers here
      ),
      effect: async (action, listenerApi) => {
        listenerApi.dispatch(setLoading(true)) // Replace with you loading indicator logic
      }
    })
    

    reference: https://redux-toolkit.js.org/api/createListenerMiddleware