import * as React from 'react' import { render, waitFor } from '@testing-library/react' import type { UseQueryResult, DefinedUseQueryResult, } from '@tanstack/react-query' import { QueryClient, useQuery, useQueries } from '@tanstack/react-query' import type { PersistedClient, Persister, } from '@tanstack/query-persist-client-core' import { persistQueryClientSave } from '@tanstack/query-persist-client-core' import { createQueryClient, mockLogger, queryKey, sleep } from './utils' import { PersistQueryClientProvider } from '../PersistQueryClientProvider' const createMockPersister = (): Persister => { let storedState: PersistedClient | undefined return { async persistClient(persistClient: PersistedClient) { storedState = persistClient }, async restoreClient() { await sleep(10) return storedState }, removeClient() { storedState = undefined }, } } const createMockErrorPersister = ( removeClient: Persister['removeClient'], ): [Error, Persister] => { const error = new Error('restore failed') return [ error, { async persistClient() { // noop }, async restoreClient() { await sleep(10) throw error }, removeClient, }, ] } describe('PersistQueryClientProvider', () => { test('restores cache from persister', async () => { const key = queryKey() const states: UseQueryResult[] = [] const queryClient = createQueryClient() await queryClient.prefetchQuery(key, () => Promise.resolve('hydrated')) const persister = createMockPersister() await persistQueryClientSave({ queryClient, persister }) queryClient.clear() function Page() { const state = useQuery(key, async () => { await sleep(10) return 'fetched' }) states.push(state) return (

{state.data}

fetchStatus: {state.fetchStatus}

) } const rendered = render( , ) await waitFor(() => rendered.getByText('fetchStatus: idle')) await waitFor(() => rendered.getByText('hydrated')) await waitFor(() => rendered.getByText('fetched')) expect(states).toHaveLength(4) expect(states[0]).toMatchObject({ status: 'loading', fetchStatus: 'idle', data: undefined, }) expect(states[1]).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: 'hydrated', }) expect(states[2]).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: 'hydrated', }) expect(states[3]).toMatchObject({ status: 'success', fetchStatus: 'idle', data: 'fetched', }) }) test('should also put useQueries into idle state', async () => { const key = queryKey() const states: UseQueryResult[] = [] const queryClient = createQueryClient() await queryClient.prefetchQuery(key, () => Promise.resolve('hydrated')) const persister = createMockPersister() await persistQueryClientSave({ queryClient, persister }) queryClient.clear() function Page() { const [state] = useQueries({ queries: [ { queryKey: key, queryFn: async (): Promise => { await sleep(10) return 'fetched' }, }, ], }) states.push(state) return (

{state.data}

fetchStatus: {state.fetchStatus}

) } const rendered = render( , ) await waitFor(() => rendered.getByText('fetchStatus: idle')) await waitFor(() => rendered.getByText('hydrated')) await waitFor(() => rendered.getByText('fetched')) expect(states).toHaveLength(4) expect(states[0]).toMatchObject({ status: 'loading', fetchStatus: 'idle', data: undefined, }) expect(states[1]).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: 'hydrated', }) expect(states[2]).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: 'hydrated', }) expect(states[3]).toMatchObject({ status: 'success', fetchStatus: 'idle', data: 'fetched', }) }) test('should show initialData while restoring', async () => { const key = queryKey() const states: DefinedUseQueryResult[] = [] const queryClient = createQueryClient() await queryClient.prefetchQuery(key, () => Promise.resolve('hydrated')) const persister = createMockPersister() await persistQueryClientSave({ queryClient, persister }) queryClient.clear() function Page() { const state = useQuery( key, async () => { await sleep(10) return 'fetched' }, { initialData: 'initial', // make sure that initial data is older than the hydration data // otherwise initialData would be newer and takes precedence initialDataUpdatedAt: 1, }, ) states.push(state) return (

{state.data}

fetchStatus: {state.fetchStatus}

) } const rendered = render( , ) await waitFor(() => rendered.getByText('initial')) await waitFor(() => rendered.getByText('hydrated')) await waitFor(() => rendered.getByText('fetched')) expect(states).toHaveLength(4) expect(states[0]).toMatchObject({ status: 'success', fetchStatus: 'idle', data: 'initial', }) expect(states[1]).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: 'hydrated', }) expect(states[2]).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: 'hydrated', }) expect(states[3]).toMatchObject({ status: 'success', fetchStatus: 'idle', data: 'fetched', }) }) test('should not refetch after restoring when data is fresh', async () => { const key = queryKey() const states: UseQueryResult[] = [] const queryClient = createQueryClient() await queryClient.prefetchQuery(key, () => Promise.resolve('hydrated')) const persister = createMockPersister() await persistQueryClientSave({ queryClient, persister }) queryClient.clear() let fetched = false function Page() { const state = useQuery( key, async () => { fetched = true await sleep(10) return 'fetched' }, { staleTime: Infinity, }, ) states.push(state) return (

data: {state.data ?? 'null'}

fetchStatus: {state.fetchStatus}

) } const rendered = render( , ) await waitFor(() => rendered.getByText('data: null')) await waitFor(() => rendered.getByText('data: hydrated')) expect(states).toHaveLength(3) expect(fetched).toBe(false) expect(states[0]).toMatchObject({ status: 'loading', fetchStatus: 'idle', data: undefined, }) expect(states[1]).toMatchObject({ status: 'success', fetchStatus: 'idle', data: 'hydrated', }) // #5443 seems like we get an extra render now ... expect(states[1]).toStrictEqual(states[2]) }) test('should call onSuccess after successful restoring', async () => { const key = queryKey() const queryClient = createQueryClient() await queryClient.prefetchQuery(key, () => Promise.resolve('hydrated')) const persister = createMockPersister() await persistQueryClientSave({ queryClient, persister }) queryClient.clear() function Page() { const state = useQuery(key, async () => { await sleep(10) return 'fetched' }) return (

{state.data}

fetchStatus: {state.fetchStatus}

) } const onSuccess = jest.fn() const rendered = render( , ) expect(onSuccess).toHaveBeenCalledTimes(0) await waitFor(() => rendered.getByText('hydrated')) expect(onSuccess).toHaveBeenCalledTimes(1) await waitFor(() => rendered.getByText('fetched')) }) test('should remove cache after non-successful restoring', async () => { const key = queryKey() jest.spyOn(console, 'warn').mockImplementation(() => undefined) jest.spyOn(console, 'error').mockImplementation(() => undefined) const queryClient = createQueryClient() const removeClient = jest.fn() const [error, persister] = createMockErrorPersister(removeClient) function Page() { const state = useQuery(key, async () => { await sleep(10) return 'fetched' }) return (

{state.data}

fetchStatus: {state.fetchStatus}

) } const rendered = render( , ) await waitFor(() => rendered.getByText('fetched')) expect(removeClient).toHaveBeenCalledTimes(1) expect(mockLogger.error).toHaveBeenCalledTimes(2) expect(mockLogger.error).toHaveBeenNthCalledWith(2, error) }) test('should be able to persist into multiple clients', async () => { const key = queryKey() const states: UseQueryResult[] = [] const queryClient = createQueryClient() await queryClient.prefetchQuery(key, () => Promise.resolve('hydrated')) const persister = createMockPersister() await persistQueryClientSave({ queryClient, persister }) queryClient.clear() const onSuccess = jest.fn() const queryFn1 = jest.fn().mockImplementation(async () => { await sleep(10) return 'queryFn1' }) const queryFn2 = jest.fn().mockImplementation(async () => { await sleep(10) return 'queryFn2' }) function App() { const [client, setClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { queryFn: queryFn1, }, }, }), ) React.useEffect(() => { setClient( new QueryClient({ defaultOptions: { queries: { queryFn: queryFn2, }, }, }), ) }, []) return ( ) } function Page() { const state = useQuery(key) states.push(state) return (

{String(state.data)}

fetchStatus: {state.fetchStatus}

) } const rendered = render() await waitFor(() => rendered.getByText('hydrated')) await waitFor(() => rendered.getByText('queryFn2')) expect(queryFn1).toHaveBeenCalledTimes(0) expect(queryFn2).toHaveBeenCalledTimes(1) expect(onSuccess).toHaveBeenCalledTimes(1) expect(states).toHaveLength(5) expect(states[0]).toMatchObject({ status: 'loading', fetchStatus: 'idle', data: undefined, }) expect(states[1]).toMatchObject({ status: 'loading', fetchStatus: 'idle', data: undefined, }) expect(states[2]).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: 'hydrated', }) expect(states[3]).toMatchObject({ status: 'success', fetchStatus: 'fetching', data: 'hydrated', }) expect(states[4]).toMatchObject({ status: 'success', fetchStatus: 'idle', data: 'queryFn2', }) }) })