Search code examples
reactjsjestjsreact-testing-libraryreact-query

Testing Optimistic update in react query


I am trying to write the test case for an optimistic update in react query. But it's not working. Here is the code that I wrote to test it. Hope someone could help me. Thanks in advance. When I just write the onSuccess and leave an optimistic update, it works fine but here it's not working. And how can we mock the getQueryData and setQueryData here?

import { act, renderHook } from "@testing-library/react-hooks";
import axios from "axios";
import { createWrapper } from "../../test-utils";
import { useAddColorHook, useFetchColorHook } from "./usePaginationReactQuery";
jest.mock("axios");

describe('Testing custom hooks of react query', () => {

    it('Should add a new color', async () => {
        axios.post.mockReturnValue({data: [{label: 'Grey', id: 23}]})
        const { result, waitFor } = renderHook(() => useAddColorHook(1), { wrapper: createWrapper() });
        await act(() => {
            result.current.mutate({ label: 'Grey' })
        })
        await waitFor(() => result.current.isSuccess);
    })
})

export const createTestQueryClient = () =>
  new QueryClient({
    defaultOptions: {
      queries: {
        retry: false,
        cacheTime: Infinity,
      },
    },
    logger: {
      log: console.log,
      warn: console.warn,
      error: () => {},
    }
  });

export function createWrapper() {
  const testQueryClient = createTestQueryClient();
  return ({ children }) => (
    <QueryClientProvider client={testQueryClient}>
      {children}
    </QueryClientProvider>
  );
}
export const useAddColorHook = (page) => {
    const queryClient = useQueryClient()
    return useMutation(addColor, {
        // onSuccess: () => {
        //     queryClient.invalidateQueries(['colors', page])
        // }
        onMutate: async color => {
            // newHero refers to the argument being passed to the mutate function
            await queryClient.cancelQueries(['colors', page])
            const previousHeroData = queryClient.getQueryData(['colors', page])
            queryClient.setQueryData(['colors', page], (oldQueryData) => {
                return {
                    ...oldQueryData,
                    data: [...oldQueryData.data, { id: oldQueryData?.data?.length + 1, ...color }]
                }
            })
            return { previousHeroData }
        },
        onSuccess: (response, variables, context) => {
            queryClient.setQueryData(['colors', page], (oldQueryData) => {
                console.log(oldQueryData, 'oldQueryData', response, 'response', variables, 'var', context, 'context', 7984)
                return {
                    ...oldQueryData,
                    data: oldQueryData.data.map(data => data.label === variables.label ? response.data : data)
                }
            })
        },
        onError: (_err, _newTodo, context) => {
            queryClient.setQueryData(['colors', page], context.previousHeroData)
        },
        onSettled: () => {
            queryClient.invalidateQueries(['colors', page])
        }
    })
}

Solution

  • The error that you are getting actually shows a bug in the way you've implemented the optimistic update:

    queryClient.setQueryData(['colors', page], (oldQueryData) => {
      return {
        ...oldQueryData,
        data: [...oldQueryData.data, { id: oldQueryData?.data?.length + 1, ...color }]
      }
    })
    

    what if there is no entry in the query cache that matches this query key? oldQueryData will be undefined, but you're not guarding against that, you are spreading ...oldQueryData.data and this will error out at runtime.

    This is what happens in your test because you start with a fresh query cache for every test.

    An easy way out would be, since you have previousHeroData already:

    const previousHeroData = queryClient.getQueryData(['colors', page])
    if (previousHeroData) {
      queryClient.setQueryData(['colors', page], {
        ...previousHeroData,
        data: [...previousHeroData.data, { id: previousHeroData.data.length + 1, ...color }]
      }
    }
    

    If you are using TanStack/query v4, you can also return undefined from the updater function. This doesn't work in v3 though:

    queryClient.setQueryData(['colors', page], (oldQueryData) => {
        return oldQueryData ? {
            ...oldQueryData,
            data: [...oldQueryData.data, { id: oldQueryData?.data?.length + 1, ...color }]
        } : undefined
    })
    

    This doesn't perform an optimistic update then though. If you know how to create a valid cache entry from undefined previous data, you can of course also do that.