import { waitFor } from '@testing-library/react'
import '@testing-library/jest-dom'

import {
  sleep,
  queryKey,
  mockLogger,
  createQueryClient,
} from '../../../../tests/utils'
import {
  InfiniteQueryObserver,
  QueryCache,
  QueryClient,
  QueryFunction,
  QueryObserver,
} from '..'
import { focusManager, onlineManager } from '..'

describe('queryClient', () => {
  let queryClient: QueryClient
  let queryCache: QueryCache

  beforeEach(() => {
    queryClient = createQueryClient()
    queryCache = queryClient.getQueryCache()
    queryClient.mount()
  })

  afterEach(() => {
    queryClient.clear()
  })

  describe('defaultOptions', () => {
    test('should merge defaultOptions', async () => {
      const key = queryKey()

      const queryFn = () => 'data'
      const testClient = createQueryClient({
        defaultOptions: { queries: { queryFn } },
      })

      expect(() => testClient.prefetchQuery(key)).not.toThrow()
    })

    test('should merge defaultOptions when query is added to cache', async () => {
      const key = queryKey()

      const testClient = createQueryClient({
        defaultOptions: {
          queries: { cacheTime: Infinity },
        },
      })

      const fetchData = () => Promise.resolve('data')
      await testClient.prefetchQuery(key, fetchData)
      const newQuery = testClient.getQueryCache().find(key)
      expect(newQuery?.options.cacheTime).toBe(Infinity)
    })

    test('should get defaultOptions', async () => {
      const queryFn = () => 'data'
      const defaultOptions = { queries: { queryFn } }
      const testClient = createQueryClient({
        defaultOptions,
      })
      expect(testClient.getDefaultOptions()).toMatchObject(defaultOptions)
    })
  })

  describe('setQueryDefaults', () => {
    test('should not trigger a fetch', async () => {
      const key = queryKey()
      queryClient.setQueryDefaults(key, { queryFn: () => 'data' })
      await sleep(1)
      const data = queryClient.getQueryData(key)
      expect(data).toBeUndefined()
    })

    test('should be able to override defaults', async () => {
      const key = queryKey()
      queryClient.setQueryDefaults(key, { queryFn: () => 'data' })
      const observer = new QueryObserver(queryClient, { queryKey: key })
      const { data } = await observer.refetch()
      expect(data).toBe('data')
    })

    test('should match the query key partially', async () => {
      const key = queryKey()
      queryClient.setQueryDefaults([key], { queryFn: () => 'data' })
      const observer = new QueryObserver(queryClient, {
        queryKey: [key, 'a'],
      })
      const { data } = await observer.refetch()
      expect(data).toBe('data')
    })

    test('should not match if the query key is a subset', async () => {
      const key = queryKey()
      queryClient.setQueryDefaults([key, 'a'], { queryFn: () => 'data' })
      const observer = new QueryObserver(queryClient, {
        queryKey: [key],
        retry: false,
        enabled: false,
      })
      const { status } = await observer.refetch()
      expect(status).toBe('error')
    })

    test('should also set defaults for observers', async () => {
      const key = queryKey()
      queryClient.setQueryDefaults(key, {
        queryFn: () => 'data',
        enabled: false,
      })
      const observer = new QueryObserver(queryClient, {
        queryKey: [key],
      })
      expect(observer.getCurrentResult().status).toBe('loading')
      expect(observer.getCurrentResult().fetchStatus).toBe('idle')
    })

    test('should update existing query defaults', async () => {
      const key = queryKey()
      const queryOptions1 = { queryFn: () => 'data' }
      const queryOptions2 = { retry: false }
      queryClient.setQueryDefaults(key, queryOptions1)
      queryClient.setQueryDefaults(key, queryOptions2)
      expect(queryClient.getQueryDefaults(key)).toMatchObject(queryOptions2)
    })

    test('should warn in dev if several query defaults match a given key', () => {
      // Check discussion here: https://github.com/tannerlinsley/react-query/discussions/3199
      const keyABCD = [
        {
          a: 'a',
          b: 'b',
          c: 'c',
          d: 'd',
        },
      ]

      // The key below "contains" keyABCD => it is more generic
      const keyABC = [
        {
          a: 'a',
          b: 'b',
          c: 'c',
        },
      ]

      // The defaults for query matching key "ABCD" (least generic)
      const defaultsOfABCD = {
        queryFn: function ABCDQueryFn() {
          return 'ABCD'
        },
      }

      // The defaults for query matching key "ABC" (most generic)
      const defaultsOfABC = {
        queryFn: function ABCQueryFn() {
          return 'ABC'
        },
      }

      // No defaults, no warning
      const noDefaults = queryClient.getQueryDefaults(keyABCD)
      expect(noDefaults).toBeUndefined()
      expect(mockLogger.error).not.toHaveBeenCalled()

      // If defaults for key ABCD are registered **before** the ones of key ABC (more generic)…
      queryClient.setQueryDefaults(keyABCD, defaultsOfABCD)
      queryClient.setQueryDefaults(keyABC, defaultsOfABC)
      // … then the "good" defaults are retrieved: we get the ones for key "ABCD"
      const goodDefaults = queryClient.getQueryDefaults(keyABCD)
      expect(goodDefaults).toBe(defaultsOfABCD)
      // The warning is still raised since several defaults are matching
      expect(mockLogger.error).toHaveBeenCalledTimes(1)

      // Let's create another queryClient and change the order of registration
      const newQueryClient = createQueryClient()
      // The defaults for key ABC (more generic) are registered **before** the ones of key ABCD…
      newQueryClient.setQueryDefaults(keyABC, defaultsOfABC)
      newQueryClient.setQueryDefaults(keyABCD, defaultsOfABCD)
      // … then the "wrong" defaults are retrieved: we get the ones for key "ABC"
      const badDefaults = newQueryClient.getQueryDefaults(keyABCD)
      expect(badDefaults).not.toBe(defaultsOfABCD)
      expect(badDefaults).toBe(defaultsOfABC)
      expect(mockLogger.error).toHaveBeenCalledTimes(2)
    })

    test('should warn in dev if several mutation defaults match a given key', () => {
      // Check discussion here: https://github.com/tannerlinsley/react-query/discussions/3199
      const keyABCD = [
        {
          a: 'a',
          b: 'b',
          c: 'c',
          d: 'd',
        },
      ]

      // The key below "contains" keyABCD => it is more generic
      const keyABC = [
        {
          a: 'a',
          b: 'b',
          c: 'c',
        },
      ]

      // The defaults for mutation matching key "ABCD" (least generic)
      const defaultsOfABCD = {
        mutationFn: Promise.resolve,
      }

      // The defaults for mutation matching key "ABC" (most generic)
      const defaultsOfABC = {
        mutationFn: Promise.resolve,
      }

      // No defaults, no warning
      const noDefaults = queryClient.getMutationDefaults(keyABCD)
      expect(noDefaults).toBeUndefined()
      expect(mockLogger.error).not.toHaveBeenCalled()

      // If defaults for key ABCD are registered **before** the ones of key ABC (more generic)…
      queryClient.setMutationDefaults(keyABCD, defaultsOfABCD)
      queryClient.setMutationDefaults(keyABC, defaultsOfABC)
      // … then the "good" defaults are retrieved: we get the ones for key "ABCD"
      const goodDefaults = queryClient.getMutationDefaults(keyABCD)
      expect(goodDefaults).toBe(defaultsOfABCD)
      // The warning is still raised since several defaults are matching
      expect(mockLogger.error).toHaveBeenCalledTimes(1)

      // Let's create another queryClient and change the order of registration
      const newQueryClient = createQueryClient()
      // The defaults for key ABC (more generic) are registered **before** the ones of key ABCD…
      newQueryClient.setMutationDefaults(keyABC, defaultsOfABC)
      newQueryClient.setMutationDefaults(keyABCD, defaultsOfABCD)
      // … then the "wrong" defaults are retrieved: we get the ones for key "ABC"
      const badDefaults = newQueryClient.getMutationDefaults(keyABCD)
      expect(badDefaults).not.toBe(defaultsOfABCD)
      expect(badDefaults).toBe(defaultsOfABC)
      expect(mockLogger.error).toHaveBeenCalledTimes(2)
    })
  })

  describe('setQueryData', () => {
    test('should not crash if query could not be found', () => {
      const key = queryKey()
      const user = { userId: 1 }
      expect(() => {
        queryClient.setQueryData([key, user], (prevUser?: typeof user) => ({
          ...prevUser!,
          name: 'Edvin',
        }))
      }).not.toThrow()
    })

    test('should not crash when variable is null', () => {
      const key = queryKey()
      queryClient.setQueryData([key, { userId: null }], 'Old Data')
      expect(() => {
        queryClient.setQueryData([key, { userId: null }], 'New Data')
      }).not.toThrow()
    })

    test('should use default options', () => {
      const key = queryKey()
      const testClient = createQueryClient({
        defaultOptions: { queries: { queryKeyHashFn: () => 'someKey' } },
      })
      const testCache = testClient.getQueryCache()
      testClient.setQueryData(key, 'data')
      expect(testClient.getQueryData(key)).toBe('data')
      expect(testCache.find(key)).toBe(testCache.get('someKey'))
    })

    test('should create a new query if query was not found', () => {
      const key = queryKey()
      queryClient.setQueryData(key, 'bar')
      expect(queryClient.getQueryData(key)).toBe('bar')
    })

    test('should create a new query if query was not found', () => {
      const key = queryKey()
      queryClient.setQueryData(key, 'qux')
      expect(queryClient.getQueryData(key)).toBe('qux')
    })

    test('should not create a new query if query was not found and data is undefined', () => {
      const key = queryKey()
      expect(queryClient.getQueryCache().find(key)).toBe(undefined)
      queryClient.setQueryData(key, undefined)
      expect(queryClient.getQueryCache().find(key)).toBe(undefined)
    })

    test('should not create a new query if query was not found and updater returns undefined', () => {
      const key = queryKey()
      expect(queryClient.getQueryCache().find(key)).toBe(undefined)
      queryClient.setQueryData(key, () => undefined)
      expect(queryClient.getQueryCache().find(key)).toBe(undefined)
    })

    test('should not update query data if data is undefined', () => {
      const key = queryKey()
      queryClient.setQueryData(key, 'qux')
      queryClient.setQueryData(key, undefined)
      expect(queryClient.getQueryData(key)).toBe('qux')
    })

    test('should not update query data if updater returns undefined', () => {
      const key = queryKey()
      queryClient.setQueryData<string>(key, 'qux')
      queryClient.setQueryData<string>(key, () => undefined)
      expect(queryClient.getQueryData(key)).toBe('qux')
    })

    test('should accept an update function', () => {
      const key = queryKey()

      const updater = jest.fn((oldData) => `new data + ${oldData}`)

      queryClient.setQueryData(key, 'test data')
      queryClient.setQueryData(key, updater)

      expect(updater).toHaveBeenCalled()
      expect(queryCache.find(key)!.state.data).toEqual('new data + test data')
    })

    test('should use prev data if an isDataEqual function is defined and returns "true"', () => {
      const key = queryKey()

      queryClient.setDefaultOptions({
        queries: { isDataEqual: (_prev, data) => data === 'data' },
      })
      queryClient.setQueryData(key, 'prev data')
      queryClient.setQueryData(key, 'data')

      expect(queryCache.find(key)!.state.data).toEqual('prev data')
    })

    test('should set the new data without comparison if structuralSharing is set to false', () => {
      const key = queryKey()

      queryClient.setDefaultOptions({
        queries: {
          structuralSharing: false,
        },
      })

      const oldData = { value: true }
      const newData = { value: true }
      queryClient.setQueryData(key, oldData)
      queryClient.setQueryData(key, newData)

      expect(queryCache.find(key)!.state.data).toBe(newData)
    })

    test('should not set isFetching to false', async () => {
      const key = queryKey()
      queryClient.prefetchQuery(key, async () => {
        await sleep(10)
        return 23
      })
      expect(queryClient.getQueryState(key)).toMatchObject({
        data: undefined,
        fetchStatus: 'fetching',
      })
      queryClient.setQueryData(key, 42)
      expect(queryClient.getQueryState(key)).toMatchObject({
        data: 42,
        fetchStatus: 'fetching',
      })
      await waitFor(() =>
        expect(queryClient.getQueryState(key)).toMatchObject({
          data: 23,
          fetchStatus: 'idle',
        }),
      )
    })
  })

  describe('setQueriesData', () => {
    test('should update all existing, matching queries', () => {
      queryClient.setQueryData(['key', 1], 1)
      queryClient.setQueryData(['key', 2], 2)

      const result = queryClient.setQueriesData<number>(['key'], (old) =>
        old ? old + 5 : undefined,
      )

      expect(result).toEqual([
        [['key', 1], 6],
        [['key', 2], 7],
      ])
      expect(queryClient.getQueryData(['key', 1])).toBe(6)
      expect(queryClient.getQueryData(['key', 2])).toBe(7)
    })

    test('should accept queryFilters', () => {
      queryClient.setQueryData(['key', 1], 1)
      queryClient.setQueryData(['key', 2], 2)
      const query1 = queryCache.find(['key', 1])!

      const result = queryClient.setQueriesData<number>(
        { predicate: (query) => query === query1 },
        (old) => old! + 5,
      )

      expect(result).toEqual([[['key', 1], 6]])
      expect(queryClient.getQueryData(['key', 1])).toBe(6)
      expect(queryClient.getQueryData(['key', 2])).toBe(2)
    })

    test('should not update non existing queries', () => {
      const result = queryClient.setQueriesData<string>(['key'], 'data')

      expect(result).toEqual([])
      expect(queryClient.getQueryData(['key'])).toBe(undefined)
    })
  })

  describe('getQueryData', () => {
    test('should return the query data if the query is found', () => {
      const key = queryKey()
      queryClient.setQueryData([key, 'id'], 'bar')
      expect(queryClient.getQueryData([key, 'id'])).toBe('bar')
    })

    test('should return undefined if the query is not found', () => {
      const key = queryKey()
      expect(queryClient.getQueryData(key)).toBeUndefined()
    })

    test('should match exact by default', () => {
      const key = queryKey()
      queryClient.setQueryData([key, 'id'], 'bar')
      expect(queryClient.getQueryData([key])).toBeUndefined()
    })
  })

  describe('getQueriesData', () => {
    test('should return the query data for all matched queries', () => {
      const key1 = queryKey()
      const key2 = queryKey()
      queryClient.setQueryData([key1, 1], 1)
      queryClient.setQueryData([key1, 2], 2)
      queryClient.setQueryData([key2, 2], 2)
      expect(queryClient.getQueriesData([key1])).toEqual([
        [[key1, 1], 1],
        [[key1, 2], 2],
      ])
    })

    test('should return empty array if queries are not found', () => {
      const key = queryKey()
      expect(queryClient.getQueriesData(key)).toEqual([])
    })

    test('should accept query filters', () => {
      queryClient.setQueryData(['key', 1], 1)
      queryClient.setQueryData(['key', 2], 2)
      const query1 = queryCache.find(['key', 1])!

      const result = queryClient.getQueriesData({
        predicate: (query) => query === query1,
      })

      expect(result).toEqual([[['key', 1], 1]])
    })
  })

  describe('fetchQuery', () => {
    test('should not type-error with strict query key', async () => {
      type StrictData = 'data'
      type StrictQueryKey = ['strict', ...ReturnType<typeof queryKey>]
      const key: StrictQueryKey = ['strict', ...queryKey()]

      const fetchFn: QueryFunction<StrictData, StrictQueryKey> = () =>
        Promise.resolve('data')

      await expect(
        queryClient.fetchQuery<StrictData, any, StrictData, StrictQueryKey>(
          key,
          fetchFn,
        ),
      ).resolves.toEqual('data')
    })

    // https://github.com/tannerlinsley/react-query/issues/652
    test('should not retry by default', async () => {
      const key = queryKey()

      await expect(
        queryClient.fetchQuery(key, async (): Promise<unknown> => {
          throw new Error('error')
        }),
      ).rejects.toEqual(new Error('error'))
    })

    test('should return the cached data on cache hit', async () => {
      const key = queryKey()

      const fetchFn = () => Promise.resolve('data')
      const first = await queryClient.fetchQuery(key, fetchFn)
      const second = await queryClient.fetchQuery(key, fetchFn)

      expect(second).toBe(first)
    })

    test('should be able to fetch when cache time is set to 0 and then be removed', async () => {
      const key1 = queryKey()
      const result = await queryClient.fetchQuery(
        key1,
        async () => {
          await sleep(10)
          return 1
        },
        { cacheTime: 0 },
      )
      expect(result).toEqual(1)
      await waitFor(() =>
        expect(queryClient.getQueryData(key1)).toEqual(undefined),
      )
    })

    test('should keep a query in cache if cache time is Infinity', async () => {
      const key1 = queryKey()
      const result = await queryClient.fetchQuery(
        key1,
        async () => {
          await sleep(10)
          return 1
        },
        { cacheTime: Infinity },
      )
      const result2 = queryClient.getQueryData(key1)
      expect(result).toEqual(1)
      expect(result2).toEqual(1)
    })

    test('should not force fetch', async () => {
      const key = queryKey()

      queryClient.setQueryData(key, 'og')
      const fetchFn = () => Promise.resolve('new')
      const first = await queryClient.fetchQuery(key, fetchFn, {
        initialData: 'initial',
        staleTime: 100,
      })
      expect(first).toBe('og')
    })

    test('should only fetch if the data is older then the given stale time', async () => {
      const key = queryKey()

      let count = 0
      const fetchFn = () => ++count

      queryClient.setQueryData(key, count)
      const first = await queryClient.fetchQuery(key, fetchFn, {
        staleTime: 100,
      })
      await sleep(11)
      const second = await queryClient.fetchQuery(key, fetchFn, {
        staleTime: 10,
      })
      const third = await queryClient.fetchQuery(key, fetchFn, {
        staleTime: 10,
      })
      await sleep(11)
      const fourth = await queryClient.fetchQuery(key, fetchFn, {
        staleTime: 10,
      })
      expect(first).toBe(0)
      expect(second).toBe(1)
      expect(third).toBe(1)
      expect(fourth).toBe(2)
    })
  })

  describe('fetchInfiniteQuery', () => {
    test('should not type-error with strict query key', async () => {
      type StrictData = string
      type StrictQueryKey = ['strict', ...ReturnType<typeof queryKey>]
      const key: StrictQueryKey = ['strict', ...queryKey()]

      const data = {
        pages: ['data'],
        pageParams: [undefined],
      } as const

      const fetchFn: QueryFunction<StrictData, StrictQueryKey> = () =>
        Promise.resolve(data.pages[0])

      await expect(
        queryClient.fetchInfiniteQuery<
          StrictData,
          any,
          StrictData,
          StrictQueryKey
        >(key, fetchFn),
      ).resolves.toEqual(data)
    })

    test('should return infinite query data', async () => {
      const key = queryKey()
      const result = await queryClient.fetchInfiniteQuery(
        key,
        ({ pageParam = 10 }) => Number(pageParam),
      )
      const result2 = queryClient.getQueryData(key)

      const expected = {
        pages: [10],
        pageParams: [undefined],
      }

      expect(result).toEqual(expected)
      expect(result2).toEqual(expected)
    })
  })

  describe('prefetchInfiniteQuery', () => {
    test('should not type-error with strict query key', async () => {
      type StrictData = 'data'
      type StrictQueryKey = ['strict', ...ReturnType<typeof queryKey>]
      const key: StrictQueryKey = ['strict', ...queryKey()]

      const fetchFn: QueryFunction<StrictData, StrictQueryKey> = () =>
        Promise.resolve('data')

      await queryClient.prefetchInfiniteQuery<
        StrictData,
        any,
        StrictData,
        StrictQueryKey
      >(key, fetchFn)

      const result = queryClient.getQueryData(key)

      expect(result).toEqual({
        pages: ['data'],
        pageParams: [undefined],
      })
    })

    test('should return infinite query data', async () => {
      const key = queryKey()

      await queryClient.prefetchInfiniteQuery(key, ({ pageParam = 10 }) =>
        Number(pageParam),
      )

      const result = queryClient.getQueryData(key)

      expect(result).toEqual({
        pages: [10],
        pageParams: [undefined],
      })
    })
  })

  describe('prefetchQuery', () => {
    test('should not type-error with strict query key', async () => {
      type StrictData = 'data'
      type StrictQueryKey = ['strict', ...ReturnType<typeof queryKey>]
      const key: StrictQueryKey = ['strict', ...queryKey()]

      const fetchFn: QueryFunction<StrictData, StrictQueryKey> = () =>
        Promise.resolve('data')

      await queryClient.prefetchQuery<
        StrictData,
        any,
        StrictData,
        StrictQueryKey
      >(key, fetchFn)

      const result = queryClient.getQueryData(key)

      expect(result).toEqual('data')
    })

    test('should return undefined when an error is thrown', async () => {
      const key = queryKey()

      const result = await queryClient.prefetchQuery(
        key,
        async (): Promise<unknown> => {
          throw new Error('error')
        },
        {
          retry: false,
        },
      )

      expect(result).toBeUndefined()
      expect(mockLogger.error).toHaveBeenCalled()
    })

    test('should be garbage collected after cacheTime if unused', async () => {
      const key = queryKey()

      await queryClient.prefetchQuery(
        key,
        async () => {
          return 'data'
        },
        { cacheTime: 10 },
      )
      expect(queryCache.find(key)).toBeDefined()
      await sleep(15)
      expect(queryCache.find(key)).not.toBeDefined()
    })
  })

  describe('removeQueries', () => {
    test('should not crash when exact is provided', async () => {
      const key = queryKey()

      const fetchFn = () => Promise.resolve('data')

      // check the query was added to the cache
      await queryClient.prefetchQuery(key, fetchFn)
      expect(queryCache.find(key)).toBeTruthy()

      // check the error doesn't occur
      expect(() =>
        queryClient.removeQueries({ queryKey: key, exact: true }),
      ).not.toThrow()

      // check query was successful removed
      expect(queryCache.find(key)).toBeFalsy()
    })
  })

  describe('cancelQueries', () => {
    test('should revert queries to their previous state', async () => {
      const key1 = queryKey()
      const key2 = queryKey()
      const key3 = queryKey()
      await queryClient.fetchQuery(key1, async () => {
        return 'data'
      })
      try {
        await queryClient.fetchQuery(key2, async () => {
          return Promise.reject<unknown>('err')
        })
      } catch {}
      queryClient.fetchQuery(key1, async () => {
        await sleep(1000)
        return 'data2'
      })
      try {
        queryClient.fetchQuery(key2, async () => {
          await sleep(1000)
          return Promise.reject<unknown>('err2')
        })
      } catch {}
      queryClient.fetchQuery(key3, async () => {
        await sleep(1000)
        return 'data3'
      })
      await sleep(10)
      await queryClient.cancelQueries()
      const state1 = queryClient.getQueryState(key1)
      const state2 = queryClient.getQueryState(key2)
      const state3 = queryClient.getQueryState(key3)
      expect(state1).toMatchObject({
        data: 'data',
        status: 'success',
      })
      expect(state2).toMatchObject({
        data: undefined,
        error: 'err',
        status: 'error',
      })
      expect(state3).toMatchObject({
        data: undefined,
        status: 'loading',
        fetchStatus: 'idle',
      })
    })

    test('should not revert if revert option is set to false', async () => {
      const key1 = queryKey()
      await queryClient.fetchQuery(key1, async () => {
        return 'data'
      })
      queryClient.fetchQuery(key1, async () => {
        await sleep(1000)
        return 'data2'
      })
      await sleep(10)
      await queryClient.cancelQueries(key1, {}, { revert: false })
      const state1 = queryClient.getQueryState(key1)
      expect(state1).toMatchObject({
        status: 'error',
      })
    })
  })

  describe('refetchQueries', () => {
    test('should not refetch if all observers are disabled', async () => {
      const key = queryKey()
      const queryFn = jest.fn<string, unknown[]>().mockReturnValue('data')
      await queryClient.fetchQuery(key, queryFn)
      const observer1 = new QueryObserver(queryClient, {
        queryKey: key,
        queryFn,
        enabled: false,
      })
      observer1.subscribe(() => undefined)
      await queryClient.refetchQueries()
      observer1.destroy()
      expect(queryFn).toHaveBeenCalledTimes(1)
    })
    test('should refetch if at least one observer is enabled', async () => {
      const key = queryKey()
      const queryFn = jest.fn<string, unknown[]>().mockReturnValue('data')
      await queryClient.fetchQuery(key, queryFn)
      const observer1 = new QueryObserver(queryClient, {
        queryKey: key,
        queryFn,
        enabled: false,
      })
      const observer2 = new QueryObserver(queryClient, {
        queryKey: key,
        queryFn,
        refetchOnMount: false,
      })
      observer1.subscribe(() => undefined)
      observer2.subscribe(() => undefined)
      await queryClient.refetchQueries()
      observer1.destroy()
      observer2.destroy()
      expect(queryFn).toHaveBeenCalledTimes(2)
    })
    test('should refetch all queries when no arguments are given', async () => {
      const key1 = queryKey()
      const key2 = queryKey()
      const queryFn1 = jest.fn<string, unknown[]>().mockReturnValue('data1')
      const queryFn2 = jest.fn<string, unknown[]>().mockReturnValue('data2')
      await queryClient.fetchQuery(key1, queryFn1)
      await queryClient.fetchQuery(key2, queryFn2)
      const observer1 = new QueryObserver(queryClient, {
        queryKey: key1,
        staleTime: Infinity,
        initialData: 'initial',
      })
      const observer2 = new QueryObserver(queryClient, {
        queryKey: key1,
        staleTime: Infinity,
        initialData: 'initial',
      })
      observer1.subscribe(() => undefined)
      observer2.subscribe(() => undefined)
      await queryClient.refetchQueries()
      observer1.destroy()
      observer2.destroy()
      expect(queryFn1).toHaveBeenCalledTimes(2)
      expect(queryFn2).toHaveBeenCalledTimes(2)
    })

    test('should be able to refetch all fresh queries', async () => {
      const key1 = queryKey()
      const key2 = queryKey()
      const queryFn1 = jest.fn<string, unknown[]>().mockReturnValue('data1')
      const queryFn2 = jest.fn<string, unknown[]>().mockReturnValue('data2')
      await queryClient.fetchQuery(key1, queryFn1)
      await queryClient.fetchQuery(key2, queryFn2)
      const observer = new QueryObserver(queryClient, {
        queryKey: key1,
        queryFn: queryFn1,
        staleTime: Infinity,
      })
      const unsubscribe = observer.subscribe(() => undefined)
      await queryClient.refetchQueries({ type: 'active', stale: false })
      unsubscribe()
      expect(queryFn1).toHaveBeenCalledTimes(2)
      expect(queryFn2).toHaveBeenCalledTimes(1)
    })

    test('should be able to refetch all stale queries', async () => {
      const key1 = queryKey()
      const key2 = queryKey()
      const queryFn1 = jest.fn<string, unknown[]>().mockReturnValue('data1')
      const queryFn2 = jest.fn<string, unknown[]>().mockReturnValue('data2')
      await queryClient.fetchQuery(key1, queryFn1)
      await queryClient.fetchQuery(key2, queryFn2)
      const observer = new QueryObserver(queryClient, {
        queryKey: key1,
        queryFn: queryFn1,
      })
      const unsubscribe = observer.subscribe(() => undefined)
      queryClient.invalidateQueries(key1)
      await queryClient.refetchQueries({ stale: true })
      unsubscribe()
      // fetchQuery, observer mount, invalidation (cancels observer mount) and refetch
      expect(queryFn1).toHaveBeenCalledTimes(4)
      expect(queryFn2).toHaveBeenCalledTimes(1)
    })

    test('should be able to refetch all stale and active queries', async () => {
      const key1 = queryKey()
      const key2 = queryKey()
      const queryFn1 = jest.fn<string, unknown[]>().mockReturnValue('data1')
      const queryFn2 = jest.fn<string, unknown[]>().mockReturnValue('data2')
      await queryClient.fetchQuery(key1, queryFn1)
      await queryClient.fetchQuery(key2, queryFn2)
      queryClient.invalidateQueries(key1)
      const observer = new QueryObserver(queryClient, {
        queryKey: key1,
        queryFn: queryFn1,
      })
      const unsubscribe = observer.subscribe(() => undefined)
      await queryClient.refetchQueries(
        { type: 'active', stale: true },
        { cancelRefetch: false },
      )
      unsubscribe()
      expect(queryFn1).toHaveBeenCalledTimes(2)
      expect(queryFn2).toHaveBeenCalledTimes(1)
    })

    test('should be able to refetch all active and inactive queries', async () => {
      const key1 = queryKey()
      const key2 = queryKey()
      const queryFn1 = jest.fn<string, unknown[]>().mockReturnValue('data1')
      const queryFn2 = jest.fn<string, unknown[]>().mockReturnValue('data2')
      await queryClient.fetchQuery(key1, queryFn1)
      await queryClient.fetchQuery(key2, queryFn2)
      const observer = new QueryObserver(queryClient, {
        queryKey: key1,
        queryFn: queryFn1,
        staleTime: Infinity,
      })
      const unsubscribe = observer.subscribe(() => undefined)
      await queryClient.refetchQueries()
      unsubscribe()
      expect(queryFn1).toHaveBeenCalledTimes(2)
      expect(queryFn2).toHaveBeenCalledTimes(2)
    })

    test('should be able to refetch all active and inactive queries', async () => {
      const key1 = queryKey()
      const key2 = queryKey()
      const queryFn1 = jest.fn<string, unknown[]>().mockReturnValue('data1')
      const queryFn2 = jest.fn<string, unknown[]>().mockReturnValue('data2')
      await queryClient.fetchQuery(key1, queryFn1)
      await queryClient.fetchQuery(key2, queryFn2)
      const observer = new QueryObserver(queryClient, {
        queryKey: key1,
        queryFn: queryFn1,
        staleTime: Infinity,
      })
      const unsubscribe = observer.subscribe(() => undefined)
      await queryClient.refetchQueries({ type: 'all' })
      unsubscribe()
      expect(queryFn1).toHaveBeenCalledTimes(2)
      expect(queryFn2).toHaveBeenCalledTimes(2)
    })

    test('should be able to refetch only active queries', async () => {
      const key1 = queryKey()
      const key2 = queryKey()
      const queryFn1 = jest.fn<string, unknown[]>().mockReturnValue('data1')
      const queryFn2 = jest.fn<string, unknown[]>().mockReturnValue('data2')
      await queryClient.fetchQuery(key1, queryFn1)
      await queryClient.fetchQuery(key2, queryFn2)
      const observer = new QueryObserver(queryClient, {
        queryKey: key1,
        queryFn: queryFn1,
        staleTime: Infinity,
      })
      const unsubscribe = observer.subscribe(() => undefined)
      await queryClient.refetchQueries({ type: 'active' })
      unsubscribe()
      expect(queryFn1).toHaveBeenCalledTimes(2)
      expect(queryFn2).toHaveBeenCalledTimes(1)
    })

    test('should be able to refetch only inactive queries', async () => {
      const key1 = queryKey()
      const key2 = queryKey()
      const queryFn1 = jest.fn<string, unknown[]>().mockReturnValue('data1')
      const queryFn2 = jest.fn<string, unknown[]>().mockReturnValue('data2')
      await queryClient.fetchQuery(key1, queryFn1)
      await queryClient.fetchQuery(key2, queryFn2)
      const observer = new QueryObserver(queryClient, {
        queryKey: key1,
        queryFn: queryFn1,
        staleTime: Infinity,
      })
      const unsubscribe = observer.subscribe(() => undefined)
      await queryClient.refetchQueries({ type: 'inactive' })
      unsubscribe()
      expect(queryFn1).toHaveBeenCalledTimes(1)
      expect(queryFn2).toHaveBeenCalledTimes(2)
    })

    test('should throw an error if throwOnError option is set to true', async () => {
      const key1 = queryKey()
      const queryFnError = () => Promise.reject<unknown>('error')
      try {
        await queryClient.fetchQuery({
          queryKey: key1,
          queryFn: queryFnError,
          retry: false,
        })
      } catch {}
      let error: any
      try {
        await queryClient.refetchQueries(
          { queryKey: key1 },
          { throwOnError: true },
        )
      } catch (err) {
        error = err
      }
      expect(error).toEqual('error')
    })
  })

  describe('invalidateQueries', () => {
    test('should refetch active queries by default', async () => {
      const key1 = queryKey()
      const key2 = queryKey()
      const queryFn1 = jest.fn<string, unknown[]>().mockReturnValue('data1')
      const queryFn2 = jest.fn<string, unknown[]>().mockReturnValue('data2')
      await queryClient.fetchQuery(key1, queryFn1)
      await queryClient.fetchQuery(key2, queryFn2)
      const observer = new QueryObserver(queryClient, {
        queryKey: key1,
        queryFn: queryFn1,
        staleTime: Infinity,
      })
      const unsubscribe = observer.subscribe(() => undefined)
      queryClient.invalidateQueries(key1)
      unsubscribe()
      expect(queryFn1).toHaveBeenCalledTimes(2)
      expect(queryFn2).toHaveBeenCalledTimes(1)
    })

    test('should not refetch inactive queries by default', async () => {
      const key1 = queryKey()
      const key2 = queryKey()
      const queryFn1 = jest.fn<string, unknown[]>().mockReturnValue('data1')
      const queryFn2 = jest.fn<string, unknown[]>().mockReturnValue('data2')
      await queryClient.fetchQuery(key1, queryFn1)
      await queryClient.fetchQuery(key2, queryFn2)
      const observer = new QueryObserver(queryClient, {
        queryKey: key1,
        enabled: false,
        staleTime: Infinity,
      })
      const unsubscribe = observer.subscribe(() => undefined)
      queryClient.invalidateQueries(key1)
      unsubscribe()
      expect(queryFn1).toHaveBeenCalledTimes(1)
      expect(queryFn2).toHaveBeenCalledTimes(1)
    })

    test('should not refetch active queries when "refetch" is "none"', async () => {
      const key1 = queryKey()
      const key2 = queryKey()
      const queryFn1 = jest.fn<string, unknown[]>().mockReturnValue('data1')
      const queryFn2 = jest.fn<string, unknown[]>().mockReturnValue('data2')
      await queryClient.fetchQuery(key1, queryFn1)
      await queryClient.fetchQuery(key2, queryFn2)
      const observer = new QueryObserver(queryClient, {
        queryKey: key1,
        queryFn: queryFn1,
        staleTime: Infinity,
      })
      const unsubscribe = observer.subscribe(() => undefined)
      queryClient.invalidateQueries(key1, {
        refetchType: 'none',
      })
      unsubscribe()
      expect(queryFn1).toHaveBeenCalledTimes(1)
      expect(queryFn2).toHaveBeenCalledTimes(1)
    })

    test('should refetch inactive queries when "refetch" is "inactive"', async () => {
      const key1 = queryKey()
      const key2 = queryKey()
      const queryFn1 = jest.fn<string, unknown[]>().mockReturnValue('data1')
      const queryFn2 = jest.fn<string, unknown[]>().mockReturnValue('data2')
      await queryClient.fetchQuery(key1, queryFn1)
      await queryClient.fetchQuery(key2, queryFn2)
      const observer = new QueryObserver(queryClient, {
        queryKey: key1,
        queryFn: queryFn1,
        staleTime: Infinity,
        refetchOnMount: false,
      })
      const unsubscribe = observer.subscribe(() => undefined)
      unsubscribe()

      await queryClient.invalidateQueries(key1, {
        refetchType: 'inactive',
      })
      expect(queryFn1).toHaveBeenCalledTimes(2)
      expect(queryFn2).toHaveBeenCalledTimes(1)
    })

    test('should refetch active and inactive queries when "refetch" is "all"', async () => {
      const key1 = queryKey()
      const key2 = queryKey()
      const queryFn1 = jest.fn<string, unknown[]>().mockReturnValue('data1')
      const queryFn2 = jest.fn<string, unknown[]>().mockReturnValue('data2')
      await queryClient.fetchQuery(key1, queryFn1)
      await queryClient.fetchQuery(key2, queryFn2)
      const observer = new QueryObserver(queryClient, {
        queryKey: key1,
        queryFn: queryFn1,
        staleTime: Infinity,
      })
      const unsubscribe = observer.subscribe(() => undefined)
      queryClient.invalidateQueries({
        refetchType: 'all',
      })
      unsubscribe()
      expect(queryFn1).toHaveBeenCalledTimes(2)
      expect(queryFn2).toHaveBeenCalledTimes(2)
    })

    test('should cancel ongoing fetches if cancelRefetch option is set (default value)', async () => {
      const key = queryKey()
      const abortFn = jest.fn()
      let fetchCount = 0
      const observer = new QueryObserver(queryClient, {
        queryKey: key,
        queryFn: ({ signal }) => {
          return new Promise((resolve) => {
            fetchCount++
            setTimeout(() => resolve(5), 10)
            if (signal) {
              signal.addEventListener('abort', abortFn)
            }
          })
        },
        initialData: 1,
      })
      observer.subscribe(() => undefined)

      await queryClient.refetchQueries()
      observer.destroy()
      if (typeof AbortSignal === 'function') {
        expect(abortFn).toHaveBeenCalledTimes(1)
      }
      expect(fetchCount).toBe(2)
    })

    test('should not cancel ongoing fetches if cancelRefetch option is set to false', async () => {
      const key = queryKey()
      const abortFn = jest.fn()
      let fetchCount = 0
      const observer = new QueryObserver(queryClient, {
        queryKey: key,
        queryFn: ({ signal }) => {
          return new Promise((resolve) => {
            fetchCount++
            setTimeout(() => resolve(5), 10)
            if (signal) {
              signal.addEventListener('abort', abortFn)
            }
          })
        },
        initialData: 1,
      })
      observer.subscribe(() => undefined)

      await queryClient.refetchQueries(undefined, { cancelRefetch: false })
      observer.destroy()
      if (typeof AbortSignal === 'function') {
        expect(abortFn).toHaveBeenCalledTimes(0)
      }
      expect(fetchCount).toBe(1)
    })
  })

  describe('resetQueries', () => {
    test('should notify listeners when a query is reset', async () => {
      const key = queryKey()

      const callback = jest.fn()

      await queryClient.prefetchQuery(key, () => 'data')

      queryCache.subscribe(callback)

      queryClient.resetQueries(key)

      expect(callback).toHaveBeenCalled()
    })

    test('should reset query', async () => {
      const key = queryKey()

      await queryClient.prefetchQuery(key, () => 'data')

      let state = queryClient.getQueryState(key)
      expect(state?.data).toEqual('data')
      expect(state?.status).toEqual('success')

      queryClient.resetQueries(key)

      state = queryClient.getQueryState(key)

      expect(state).toBeTruthy()
      expect(state?.data).toBeUndefined()
      expect(state?.status).toEqual('loading')
      expect(state?.fetchStatus).toEqual('idle')
    })

    test('should reset query data to initial data if set', async () => {
      const key = queryKey()

      await queryClient.prefetchQuery(key, () => 'data', {
        initialData: 'initial',
      })

      let state = queryClient.getQueryState(key)
      expect(state?.data).toEqual('data')

      queryClient.resetQueries(key)

      state = queryClient.getQueryState(key)

      expect(state).toBeTruthy()
      expect(state?.data).toEqual('initial')
    })

    test('should refetch all active queries', async () => {
      const key1 = queryKey()
      const key2 = queryKey()
      const queryFn1 = jest.fn<string, unknown[]>().mockReturnValue('data1')
      const queryFn2 = jest.fn<string, unknown[]>().mockReturnValue('data2')
      const observer1 = new QueryObserver(queryClient, {
        queryKey: key1,
        queryFn: queryFn1,
        enabled: true,
      })
      const observer2 = new QueryObserver(queryClient, {
        queryKey: key2,
        queryFn: queryFn2,
        enabled: false,
      })
      observer1.subscribe(() => undefined)
      observer2.subscribe(() => undefined)
      await queryClient.resetQueries()
      observer2.destroy()
      observer1.destroy()
      expect(queryFn1).toHaveBeenCalledTimes(2)
      expect(queryFn2).toHaveBeenCalledTimes(0)
    })
  })

  describe('refetch only certain pages of an infinite query', () => {
    test('refetchQueries', async () => {
      const key = queryKey()
      let multiplier = 1
      const observer = new InfiniteQueryObserver<number>(queryClient, {
        queryKey: key,
        queryFn: ({ pageParam = 10 }) => Number(pageParam) * multiplier,
        getNextPageParam: (lastPage) => lastPage + 1,
      })

      await observer.fetchNextPage()
      await observer.fetchNextPage()

      expect(queryClient.getQueryData(key)).toMatchObject({
        pages: [10, 11],
      })

      multiplier = 2

      await queryClient.refetchQueries({
        queryKey: key,
        refetchPage: (_, index) => index === 0,
      })

      expect(queryClient.getQueryData(key)).toMatchObject({
        pages: [20, 11],
      })
    })
    test('invalidateQueries', async () => {
      const key = queryKey()
      let multiplier = 1
      const observer = new InfiniteQueryObserver<number>(queryClient, {
        queryKey: key,
        queryFn: ({ pageParam = 10 }) => Number(pageParam) * multiplier,
        getNextPageParam: (lastPage) => lastPage + 1,
      })

      await observer.fetchNextPage()
      await observer.fetchNextPage()

      expect(queryClient.getQueryData(key)).toMatchObject({
        pages: [10, 11],
      })

      multiplier = 2

      await queryClient.invalidateQueries({
        queryKey: key,
        refetchType: 'all',
        refetchPage: (page, _, allPages) => {
          return page === allPages[0]
        },
      })

      expect(queryClient.getQueryData(key)).toMatchObject({
        pages: [20, 11],
      })
    })

    test('resetQueries', async () => {
      const key = queryKey()
      let multiplier = 1
      new InfiniteQueryObserver<number>(queryClient, {
        queryKey: key,
        queryFn: ({ pageParam = 10 }) => Number(pageParam) * multiplier,
        getNextPageParam: (lastPage) => lastPage + 1,
        initialData: () => ({
          pages: [10, 11],
          pageParams: [10, 11],
        }),
      })

      expect(queryClient.getQueryData(key)).toMatchObject({
        pages: [10, 11],
      })

      multiplier = 2

      await queryClient.resetQueries({
        queryKey: key,
        type: 'inactive',
        refetchPage: (page, _, allPages) => {
          return page === allPages[0]
        },
      })

      expect(queryClient.getQueryData(key)).toMatchObject({
        pages: [20, 11],
      })
    })
  })

  describe('focusManager and onlineManager', () => {
    test('should notify queryCache and mutationCache if focused', async () => {
      const testClient = createQueryClient()
      testClient.mount()

      const queryCacheOnFocusSpy = jest.spyOn(
        testClient.getQueryCache(),
        'onFocus',
      )
      const queryCacheOnOnlineSpy = jest.spyOn(
        testClient.getQueryCache(),
        'onOnline',
      )
      const mutationCacheResumePausedMutationsSpy = jest.spyOn(
        testClient.getMutationCache(),
        'resumePausedMutations',
      )

      focusManager.setFocused(false)
      expect(queryCacheOnFocusSpy).not.toHaveBeenCalled()
      expect(mutationCacheResumePausedMutationsSpy).not.toHaveBeenCalled()

      focusManager.setFocused(true)
      expect(queryCacheOnFocusSpy).toHaveBeenCalledTimes(1)
      expect(mutationCacheResumePausedMutationsSpy).toHaveBeenCalledTimes(1)

      expect(queryCacheOnOnlineSpy).not.toHaveBeenCalled()

      queryCacheOnFocusSpy.mockRestore()
      mutationCacheResumePausedMutationsSpy.mockRestore()
      queryCacheOnOnlineSpy.mockRestore()
      focusManager.setFocused(undefined)
    })

    test('should notify queryCache and mutationCache if online', async () => {
      const testClient = createQueryClient()
      testClient.mount()

      const queryCacheOnFocusSpy = jest.spyOn(
        testClient.getQueryCache(),
        'onFocus',
      )
      const queryCacheOnOnlineSpy = jest.spyOn(
        testClient.getQueryCache(),
        'onOnline',
      )
      const mutationCacheResumePausedMutationsSpy = jest.spyOn(
        testClient.getMutationCache(),
        'resumePausedMutations',
      )

      onlineManager.setOnline(false)
      expect(queryCacheOnOnlineSpy).not.toHaveBeenCalled()
      expect(mutationCacheResumePausedMutationsSpy).not.toHaveBeenCalled()

      onlineManager.setOnline(true)
      expect(queryCacheOnOnlineSpy).toHaveBeenCalledTimes(1)
      expect(mutationCacheResumePausedMutationsSpy).toHaveBeenCalledTimes(1)

      expect(queryCacheOnFocusSpy).not.toHaveBeenCalled()

      queryCacheOnFocusSpy.mockRestore()
      queryCacheOnOnlineSpy.mockRestore()
      mutationCacheResumePausedMutationsSpy.mockRestore()
      onlineManager.setOnline(undefined)
    })
  })

  describe('setMutationDefaults', () => {
    test('should update existing mutation defaults', () => {
      const key = queryKey()
      const mutationOptions1 = { mutationFn: async () => 'data' }
      const mutationOptions2 = { retry: false }
      queryClient.setMutationDefaults(key, mutationOptions1)
      queryClient.setMutationDefaults(key, mutationOptions2)
      expect(queryClient.getMutationDefaults(key)).toMatchObject(
        mutationOptions2,
      )
    })
  })
})


"test:eslint": "../../node_modules/.bin/eslint --ext .ts,.tsx ./src",

"compile": "../../node_modules/.bin/tsc -p tsconfig.json --noEmit --emitDeclarationOnly false"

keep keep keep keep replace keep keep keep keep keep

import * as React from 'react'
import ReactDOM from 'react-dom'
import ReactDOMTestUtils from 'react-dom/test-utils'
import ReactDOMServer from 'react-dom/server'
// eslint-disable-next-line import/no-unresolved -- types only for module augmentation
import type {} from 'react-dom/next'

import {
  useQuery,
  QueryClientProvider,
  QueryCache,
  dehydrate,
  hydrate,
} from '..'
import {
  createQueryClient,
  mockLogger,
  setIsServer,
  sleep,
} from '../../../../tests/utils'

const isReact18 = () => (process.env.REACTJS_VERSION || '18') === '18'

const ReactHydrate = (element: React.ReactElement, container: Element) => {
  if (isReact18()) {
    let root: any
    ReactDOMTestUtils.act(() => {
      // @ts-expect-error
      root = ReactDOM.hydrateRoot(container, element)
    })
    return () => {
      root.unmount()
    }
  }

  ReactDOM.hydrate(element, container)
  return () => {
    ReactDOM.unmountComponentAtNode(container)
  }
}

async function fetchData<TData>(value: TData, ms?: number): Promise<TData> {
  await sleep(ms || 1)
  return value
}

function PrintStateComponent({ componentName, result }: any): any {
  return `${componentName} - status:${result.status} fetching:${result.isFetching} data:${result.data}`
}

describe('Server side rendering with de/rehydration', () => {
  let previousIsReactActEnvironment: unknown
  beforeAll(() => {
    // @ts-expect-error we expect IS_REACT_ACT_ENVIRONMENT to exist
    previousIsReactActEnvironment = globalThis.IS_REACT_ACT_ENVIRONMENT = true
  })

  afterAll(() => {
    // @ts-expect-error we expect IS_REACT_ACT_ENVIRONMENT to exist
    globalThis.IS_REACT_ACT_ENVIRONMENT = previousIsReactActEnvironment
  })
  it('should not mismatch on success', async () => {
    if (!isReact18()) {
      return
    }
    const fetchDataSuccess = jest.fn<
      ReturnType<typeof fetchData>,
      Parameters<typeof fetchData>
    >(fetchData)

    // -- Shared part --
    function SuccessComponent() {
      const result = useQuery(['success'], () => fetchDataSuccess('success!'))
      return (
        <PrintStateComponent componentName="SuccessComponent" result={result} />
      )
    }

    // -- Server part --
    setIsServer(true)

    const prefetchCache = new QueryCache()
    const prefetchClient = createQueryClient({
      queryCache: prefetchCache,
    })
    await prefetchClient.prefetchQuery(['success'], () =>
      fetchDataSuccess('success'),
    )
    const dehydratedStateServer = dehydrate(prefetchClient)
    const renderCache = new QueryCache()
    const renderClient = createQueryClient({
      queryCache: renderCache,
    })
    hydrate(renderClient, dehydratedStateServer)
    const markup = ReactDOMServer.renderToString(
      <QueryClientProvider client={renderClient}>
        <SuccessComponent />
      </QueryClientProvider>,
    )
    const stringifiedState = JSON.stringify(dehydratedStateServer)
    renderClient.clear()
    setIsServer(false)

    const expectedMarkup =
      'SuccessComponent - status:success fetching:true data:success'

    expect(markup).toBe(expectedMarkup)
    expect(fetchDataSuccess).toHaveBeenCalledTimes(1)

    // -- Client part --
    const el = document.createElement('div')
    el.innerHTML = markup

    const queryCache = new QueryCache()
    const queryClient = createQueryClient({ queryCache })
    hydrate(queryClient, JSON.parse(stringifiedState))

    const unmount = ReactHydrate(
      <QueryClientProvider client={queryClient}>
        <SuccessComponent />
      </QueryClientProvider>,
      el,
    )

    // Check that we have no React hydration mismatches
    expect(mockLogger.error).not.toHaveBeenCalled()
    expect(fetchDataSuccess).toHaveBeenCalledTimes(2)
    expect(el.innerHTML).toBe(expectedMarkup)

    unmount()
    queryClient.clear()
  })

  it('should not mismatch on error', async () => {
    if (!isReact18()) {
      return
    }
    const fetchDataError = jest.fn(() => {
      throw new Error('fetchDataError')
    })

    // -- Shared part --
    function ErrorComponent() {
      const result = useQuery(['error'], () => fetchDataError(), {
        retry: false,
      })
      return (
        <PrintStateComponent componentName="ErrorComponent" result={result} />
      )
    }

    // -- Server part --
    setIsServer(true)
    const prefetchCache = new QueryCache()
    const prefetchClient = createQueryClient({
      queryCache: prefetchCache,
    })
    await prefetchClient.prefetchQuery(['error'], () => fetchDataError())
    const dehydratedStateServer = dehydrate(prefetchClient)
    const renderCache = new QueryCache()
    const renderClient = createQueryClient({
      queryCache: renderCache,
    })
    hydrate(renderClient, dehydratedStateServer)
    const markup = ReactDOMServer.renderToString(
      <QueryClientProvider client={renderClient}>
        <ErrorComponent />
      </QueryClientProvider>,
    )
    const stringifiedState = JSON.stringify(dehydratedStateServer)
    renderClient.clear()
    setIsServer(false)

    const expectedMarkup =
      'ErrorComponent - status:loading fetching:true data:undefined'

    expect(markup).toBe(expectedMarkup)

    // -- Client part --
    const el = document.createElement('div')
    el.innerHTML = markup

    const queryCache = new QueryCache()
    const queryClient = createQueryClient({ queryCache })
    hydrate(queryClient, JSON.parse(stringifiedState))

    const unmount = ReactHydrate(
      <QueryClientProvider client={queryClient}>
        <ErrorComponent />
      </QueryClientProvider>,
      el,
    )

    // We expect exactly one console.error here, which is from the
    expect(mockLogger.error).toHaveBeenCalledTimes(1)
    expect(fetchDataError).toHaveBeenCalledTimes(2)
    expect(el.innerHTML).toBe(expectedMarkup)
    await sleep(50)
    expect(fetchDataError).toHaveBeenCalledTimes(2)
    expect(el.innerHTML).toBe(
      'ErrorComponent - status:error fetching:false data:undefined',
    )

    unmount()
    queryClient.clear()
  })

  it('should not mismatch on queries that were not prefetched', async () => {
    if (!isReact18()) {
      return
    }
    const fetchDataSuccess = jest.fn<
      ReturnType<typeof fetchData>,
      Parameters<typeof fetchData>
    >(fetchData)

    // -- Shared part --
    function SuccessComponent() {
      const result = useQuery(['success'], () => fetchDataSuccess('success!'))
      return (
        <PrintStateComponent componentName="SuccessComponent" result={result} />
      )
    }

    // -- Server part --
    setIsServer(true)

    const prefetchClient = createQueryClient()
    const dehydratedStateServer = dehydrate(prefetchClient)
    const renderClient = createQueryClient()
    hydrate(renderClient, dehydratedStateServer)
    const markup = ReactDOMServer.renderToString(
      <QueryClientProvider client={renderClient}>
        <SuccessComponent />
      </QueryClientProvider>,
    )
    const stringifiedState = JSON.stringify(dehydratedStateServer)
    renderClient.clear()
    setIsServer(false)

    const expectedMarkup =
      'SuccessComponent - status:loading fetching:true data:undefined'

    expect(markup).toBe(expectedMarkup)

    // -- Client part --
    const el = document.createElement('div')
    el.innerHTML = markup

    const queryCache = new QueryCache()
    const queryClient = createQueryClient({ queryCache })
    hydrate(queryClient, JSON.parse(stringifiedState))

    const unmount = ReactHydrate(
      <QueryClientProvider client={queryClient}>
        <SuccessComponent />
      </QueryClientProvider>,
      el,
    )

    // Check that we have no React hydration mismatches
    expect(mockLogger.error).not.toHaveBeenCalled()
    expect(fetchDataSuccess).toHaveBeenCalledTimes(1)
    expect(el.innerHTML).toBe(expectedMarkup)
    await sleep(50)
    expect(fetchDataSuccess).toHaveBeenCalledTimes(1)
    expect(el.innerHTML).toBe(
      'SuccessComponent - status:success fetching:false data:success!',
    )

    unmount()
    queryClient.clear()
  })
})


// eslint-disable-next-line @typescript-eslint/no-var-requires
const defineTest = require('jscodeshift/dist/testUtils').defineTest

jest.autoMockOff()

defineTest(__dirname, 'key-transformation', null, 'default-import', {
  parser: 'tsx',
})

defineTest(__dirname, 'key-transformation', null, 'named-import', {
  parser: 'tsx',
})

defineTest(__dirname, 'key-transformation', null, 'namespaced-import', {
  parser: 'tsx',
})

defineTest(__dirname, 'key-transformation', null, 'parameter-is-identifier', {
  parser: 'tsx',
})

defineTest(
  __dirname,
  'key-transformation',
  null,
  'parameter-is-object-expression',
  {
    parser: 'tsx',
  }
)

defineTest(__dirname, 'key-transformation', null, 'type-arguments', {
  parser: 'tsx',
})


keep keep keep replace keep keep keep keep

@tanstack/query-sync-storage-persister

TODO

../../node_modules/.bin/eslint --ext .ts,.tsx ./src

../../node_modules/.bin/tsc -p tsconfig.json --noEmit --emitDeclarationOnly false


# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build

yarn.lock
package-lock.json

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*


import {
  parseQueryArgs,
  QueryFunction,
  QueryKey,
  QueryObserver,
} from '@tanstack/query-core'
import { DefinedUseQueryResult, UseQueryOptions, UseQueryResult } from './types'
import { useBaseQuery } from './useBaseQuery'

// HOOK

export function useQuery<
  TQueryFnData = unknown,
  TError = unknown,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
>(
  options: Omit<
    UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
    'initialData'
  > & { initialData?: () => undefined },
): UseQueryResult<TData, TError>

export function useQuery<
  TQueryFnData = unknown,
  TError = unknown,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
>(
  options: Omit<
    UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
    'initialData'
  > & { initialData: TQueryFnData | (() => TQueryFnData) },
): DefinedUseQueryResult<TData, TError>

export function useQuery<
  TQueryFnData = unknown,
  TError = unknown,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
>(
  queryKey: TQueryKey,
  options?: Omit<
    UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
    'queryKey' | 'initialData'
  > & { initialData?: () => undefined },
): UseQueryResult<TData, TError>

export function useQuery<
  TQueryFnData = unknown,
  TError = unknown,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
>(
  queryKey: TQueryKey,
  options?: Omit<
    UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
    'queryKey' | 'initialData'
  > & { initialData: TQueryFnData | (() => TQueryFnData) },
): DefinedUseQueryResult<TData, TError>

export function useQuery<
  TQueryFnData = unknown,
  TError = unknown,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
>(
  queryKey: TQueryKey,
  queryFn: QueryFunction<TQueryFnData, TQueryKey>,
  options?: Omit<
    UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
    'queryKey' | 'queryFn' | 'initialData'
  > & { initialData?: () => undefined },
): UseQueryResult<TData, TError>

export function useQuery<
  TQueryFnData = unknown,
  TError = unknown,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
>(
  queryKey: TQueryKey,
  queryFn: QueryFunction<TQueryFnData, TQueryKey>,
  options?: Omit<
    UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
    'queryKey' | 'queryFn' | 'initialData'
  > & { initialData: TQueryFnData | (() => TQueryFnData) },
): DefinedUseQueryResult<TData, TError>

export function useQuery<
  TQueryFnData,
  TError,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
>(
  arg1: TQueryKey | UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
  arg2?:
    | QueryFunction<TQueryFnData, TQueryKey>
    | UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
  arg3?: UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
): UseQueryResult<TData, TError> {
  const parsedOptions = parseQueryArgs(arg1, arg2, arg3)
  return useBaseQuery(parsedOptions, QueryObserver)
}


keep keep keep keep keep replace keep keep keep keep keep

@tanstack/query-core

TODO


import { fireEvent, waitFor } from '@testing-library/react'
import * as React from 'react'
import { ErrorBoundary } from 'react-error-boundary'

import * as QueriesObserverModule from '../../../query-core/src/queriesObserver'

import {
  createQueryClient,
  expectType,
  expectTypeNotAny,
  queryKey,
  sleep,
} from '../../../../tests/utils'
import { renderWithClient } from './utils'
import {
  QueryClient,
  useQueries,
  UseQueryResult,
  QueryCache,
  QueryObserverResult,
  QueriesObserver,
  QueryFunction,
  UseQueryOptions,
  QueryKey,
} from '..'
import { QueryFunctionContext } from '@tanstack/query-core'

describe('useQueries', () => {
  const queryCache = new QueryCache()
  const queryClient = createQueryClient({ queryCache })

  it('should return the correct states', async () => {
    const key1 = queryKey()
    const key2 = queryKey()
    const results: UseQueryResult[][] = []

    function Page() {
      const result = useQueries({
        queries: [
          {
            queryKey: key1,
            queryFn: async () => {
              await sleep(10)
              return 1
            },
          },
          {
            queryKey: key2,
            queryFn: async () => {
              await sleep(100)
              return 2
            },
          },
        ],
      })
      results.push(result)

      return (

            data1: {String(result[0].data ?? 'null')}, data2:{' '}
            {String(result[1].data ?? 'null')}

      )
    }

    const rendered = renderWithClient(queryClient, <Page />)

    await waitFor(() => rendered.getByText('data1: 1, data2: 2'))

    expect(results.length).toBe(3)
    expect(results[0]).toMatchObject([{ data: undefined }, { data: undefined }])
    expect(results[1]).toMatchObject([{ data: 1 }, { data: undefined }])
    expect(results[2]).toMatchObject([{ data: 1 }, { data: 2 }])
  })

  it('should keep previous data if amount of queries is the same', async () => {
    const key1 = queryKey()
    const key2 = queryKey()
    const states: UseQueryResult[][] = []

    function Page() {
      const [count, setCount] = React.useState(1)
      const result = useQueries({
        queries: [
          {
            queryKey: [key1, count],
            keepPreviousData: true,
            queryFn: async () => {
              await sleep(10)
              return count * 2
            },
          },
          {
            queryKey: [key2, count],
            keepPreviousData: true,
            queryFn: async () => {
              await sleep(35)
              return count * 5
            },
          },
        ],
      })
      states.push(result)

      const isFetching = result.some((r) => r.isFetching)

      return (

            data1: {String(result[0].data ?? 'null')}, data2:{' '}
            {String(result[1].data ?? 'null')}

isFetching: {String(isFetching)}

          <button onClick={() => setCount((prev) => prev + 1)}>inc</button>

      )
    }

    const rendered = renderWithClient(queryClient, <Page />)

    await waitFor(() => rendered.getByText('data1: 2, data2: 5'))
    fireEvent.click(rendered.getByRole('button', { name: /inc/i }))

    await waitFor(() => rendered.getByText('data1: 4, data2: 10'))
    await waitFor(() => rendered.getByText('isFetching: false'))

    expect(states[states.length - 1]).toMatchObject([
      { status: 'success', data: 4, isPreviousData: false, isFetching: false },
      { status: 'success', data: 10, isPreviousData: false, isFetching: false },
    ])
  })

  it('should keep previous data for variable amounts of useQueries', async () => {
    const key = queryKey()
    const states: UseQueryResult[][] = []

    function Page() {
      const [count, setCount] = React.useState(2)
      const result = useQueries({
        queries: Array.from({ length: count }, (_, i) => ({
          queryKey: [key, count, i + 1],
          keepPreviousData: true,
          queryFn: async () => {
            await sleep(35 * (i + 1))
            return (i + 1) * count * 2
          },
        })),
      })

      states.push(result)

      const isFetching = result.some((r) => r.isFetching)

      return (

data: {result.map((it) => it.data).join(',')}

isFetching: {String(isFetching)}

          <button onClick={() => setCount((prev) => prev + 1)}>inc</button>

      )
    }

    const rendered = renderWithClient(queryClient, <Page />)

    await waitFor(() => rendered.getByText('data: 4,8'))
    fireEvent.click(rendered.getByRole('button', { name: /inc/i }))

    await waitFor(() => rendered.getByText('data: 6,12,18'))
    await waitFor(() => rendered.getByText('isFetching: false'))

    expect(states[states.length - 1]).toMatchObject([
      { status: 'success', data: 6, isPreviousData: false, isFetching: false },
      { status: 'success', data: 12, isPreviousData: false, isFetching: false },
      { status: 'success', data: 18, isPreviousData: false, isFetching: false },
    ])
  })

  it('should keep previous data when switching between queries', async () => {
    const key = queryKey()
    const states: UseQueryResult[][] = []

    function Page() {
      const [series1, setSeries1] = React.useState(1)
      const [series2, setSeries2] = React.useState(2)
      const ids = [series1, series2]

      const result = useQueries({
        queries: ids.map((id) => {
          return {
            queryKey: [key, id],
            queryFn: async () => {
              await sleep(5)
              return id * 5
            },
            keepPreviousData: true,
          }
        }),
      })

      states.push(result)

      const isFetching = result.some((r) => r.isFetching)

      return (

            data1: {String(result[0]?.data ?? 'null')}, data2:{' '}
            {String(result[1]?.data ?? 'null')}

isFetching: {String(isFetching)}

          <button onClick={() => setSeries2(3)}>setSeries2</button>
          <button onClick={() => setSeries1(2)}>setSeries1</button>

      )
    }

    const rendered = renderWithClient(queryClient, <Page />)

    await waitFor(() => rendered.getByText('data1: 5, data2: 10'))
    fireEvent.click(rendered.getByRole('button', { name: /setSeries2/i }))

    await waitFor(() => rendered.getByText('data1: 5, data2: 15'))
    fireEvent.click(rendered.getByRole('button', { name: /setSeries1/i }))

    await waitFor(() => rendered.getByText('data1: 10, data2: 15'))
    await waitFor(() => rendered.getByText('isFetching: false'))

    expect(states[states.length - 1]).toMatchObject([
      { status: 'success', data: 10, isPreviousData: false, isFetching: false },
      { status: 'success', data: 15, isPreviousData: false, isFetching: false },
    ])
  })

  it('should not go to infinite render loop with previous data when toggling queries', async () => {
    const key = queryKey()
    const states: UseQueryResult[][] = []

    function Page() {
      const [enableId1, setEnableId1] = React.useState(true)
      const ids = enableId1 ? [1, 2] : [2]

      const result = useQueries({
        queries: ids.map((id) => {
          return {
            queryKey: [key, id],
            queryFn: async () => {
              await sleep(5)
              return id * 5
            },
            keepPreviousData: true,
          }
        }),
      })

      states.push(result)

      const isFetching = result.some((r) => r.isFetching)

      return (

            data1: {String(result[0]?.data ?? 'null')}, data2:{' '}
            {String(result[1]?.data ?? 'null')}

isFetching: {String(isFetching)}

          <button onClick={() => setEnableId1(false)}>set1Disabled</button>
          <button onClick={() => setEnableId1(true)}>set2Enabled</button>

      )
    }

    const rendered = renderWithClient(queryClient, <Page />)

    await waitFor(() => rendered.getByText('data1: 5, data2: 10'))
    fireEvent.click(rendered.getByRole('button', { name: /set1Disabled/i }))

    await waitFor(() => rendered.getByText('data1: 10, data2: null'))
    await waitFor(() => rendered.getByText('isFetching: false'))
    fireEvent.click(rendered.getByRole('button', { name: /set2Enabled/i }))

    await waitFor(() => rendered.getByText('data1: 5, data2: 10'))
    await waitFor(() => rendered.getByText('isFetching: false'))

    await waitFor(() => expect(states.length).toBe(6))

    expect(states[0]).toMatchObject([
      {
        status: 'loading',
        data: undefined,
        isPreviousData: false,
        isFetching: true,
      },
      {
        status: 'loading',
        data: undefined,
        isPreviousData: false,
        isFetching: true,
      },
    ])
    expect(states[1]).toMatchObject([
      { status: 'success', data: 5, isPreviousData: false, isFetching: false },
      { status: 'success', data: 10, isPreviousData: false, isFetching: false },
    ])
    expect(states[2]).toMatchObject([
      { status: 'success', data: 10, isPreviousData: false, isFetching: false },
    ])
    expect(states[3]).toMatchObject([
      { status: 'success', data: 5, isPreviousData: false, isFetching: true },
      { status: 'success', data: 10, isPreviousData: false, isFetching: false },
    ])
    expect(states[4]).toMatchObject([
      { status: 'success', data: 5, isPreviousData: false, isFetching: true },
      { status: 'success', data: 10, isPreviousData: false, isFetching: false },
    ])
    expect(states[5]).toMatchObject([
      { status: 'success', data: 5, isPreviousData: false, isFetching: false },
      { status: 'success', data: 10, isPreviousData: false, isFetching: false },
    ])
  })

  it('handles type parameter - tuple of tuples', async () => {
    const key1 = queryKey()
    const key2 = queryKey()
    const key3 = queryKey()

    // @ts-expect-error (Page component is not rendered)
    // eslint-disable-next-line
    function Page() {
      const result1 = useQueries<[[number], [string], [string[], boolean]]>({
        queries: [
          {
            queryKey: key1,
            queryFn: () => 1,
          },
          {
            queryKey: key2,
            queryFn: () => 'string',
          },
          {
            queryKey: key3,
            queryFn: () => ['string[]'],
          },
        ],
      })
      expectType<QueryObserverResult<number, unknown>>(result1[0])
      expectType<QueryObserverResult<string, unknown>>(result1[1])
      expectType<QueryObserverResult<string[], boolean>>(result1[2])
      expectType<number | undefined>(result1[0].data)
      expectType<string | undefined>(result1[1].data)
      expectType<string[] | undefined>(result1[2].data)
      expectType<boolean | null>(result1[2].error)

      // TData (3rd element) takes precedence over TQueryFnData (1st element)
      const result2 = useQueries<
        [[string, unknown, string], [string, unknown, number]]
      >({
        queries: [
          {
            queryKey: key1,
            queryFn: () => 'string',
            select: (a) => {
              expectType<string>(a)
              expectTypeNotAny(a)
              return a.toLowerCase()
            },
          },
          {
            queryKey: key2,
            queryFn: () => 'string',
            select: (a) => {
              expectType<string>(a)
              expectTypeNotAny(a)
              return parseInt(a)
            },
          },
        ],
      })
      expectType<QueryObserverResult<string, unknown>>(result2[0])
      expectType<QueryObserverResult<number, unknown>>(result2[1])
      expectType<string | undefined>(result2[0].data)
      expectType<number | undefined>(result2[1].data)

      // types should be enforced
      useQueries<[[string, unknown, string], [string, boolean, number]]>({
        queries: [
          {
            queryKey: key1,
            queryFn: () => 'string',
            select: (a) => {
              expectType<string>(a)
              expectTypeNotAny(a)
              return a.toLowerCase()
            },
            onSuccess: (a) => {
              expectType<string>(a)
              expectTypeNotAny(a)
            },
            placeholderData: 'string',
            // @ts-expect-error (initialData: string)
            initialData: 123,
          },
          {
            queryKey: key2,
            queryFn: () => 'string',
            select: (a) => {
              expectType<string>(a)
              expectTypeNotAny(a)
              return parseInt(a)
            },
            onSuccess: (a) => {
              expectType<number>(a)
              expectTypeNotAny(a)
            },
            onError: (e) => {
              expectType<boolean>(e)
              expectTypeNotAny(e)
            },
            placeholderData: 'string',
            // @ts-expect-error (initialData: string)
            initialData: 123,
          },
        ],
      })

      // field names should be enforced
      useQueries<[[string]]>({
        queries: [
          {
            queryKey: key1,
            queryFn: () => 'string',
            // @ts-expect-error (invalidField)
            someInvalidField: [],
          },
        ],
      })
    }
  })

  it('handles type parameter - tuple of objects', async () => {
    const key1 = queryKey()
    const key2 = queryKey()
    const key3 = queryKey()

    // @ts-expect-error (Page component is not rendered)
    // eslint-disable-next-line
    function Page() {
      const result1 = useQueries<
        [
          { queryFnData: number },
          { queryFnData: string },
          { queryFnData: string[]; error: boolean },
        ]
      >({
        queries: [
          {
            queryKey: key1,
            queryFn: () => 1,
          },
          {
            queryKey: key2,
            queryFn: () => 'string',
          },
          {
            queryKey: key3,
            queryFn: () => ['string[]'],
          },
        ],
      })
      expectType<QueryObserverResult<number, unknown>>(result1[0])
      expectType<QueryObserverResult<string, unknown>>(result1[1])
      expectType<QueryObserverResult<string[], boolean>>(result1[2])
      expectType<number | undefined>(result1[0].data)
      expectType<string | undefined>(result1[1].data)
      expectType<string[] | undefined>(result1[2].data)
      expectType<boolean | null>(result1[2].error)

      // TData (data prop) takes precedence over TQueryFnData (queryFnData prop)
      const result2 = useQueries<
        [
          { queryFnData: string; data: string },
          { queryFnData: string; data: number },
        ]
      >({
        queries: [
          {
            queryKey: key1,
            queryFn: () => 'string',
            select: (a) => {
              expectType<string>(a)
              expectTypeNotAny(a)
              return a.toLowerCase()
            },
          },
          {
            queryKey: key2,
            queryFn: () => 'string',
            select: (a) => {
              expectType<string>(a)
              expectTypeNotAny(a)
              return parseInt(a)
            },
          },
        ],
      })
      expectType<QueryObserverResult<string, unknown>>(result2[0])
      expectType<QueryObserverResult<number, unknown>>(result2[1])
      expectType<string | undefined>(result2[0].data)
      expectType<number | undefined>(result2[1].data)

      // can pass only TData (data prop) although TQueryFnData will be left unknown
      const result3 = useQueries<[{ data: string }, { data: number }]>({
        queries: [
          {
            queryKey: key1,
            queryFn: () => 'string',
            select: (a) => {
              expectType<unknown>(a)
              expectTypeNotAny(a)
              return a as string
            },
          },
          {
            queryKey: key2,
            queryFn: () => 'string',
            select: (a) => {
              expectType<unknown>(a)
              expectTypeNotAny(a)
              return a as number
            },
          },
        ],
      })
      expectType<QueryObserverResult<string, unknown>>(result3[0])
      expectType<QueryObserverResult<number, unknown>>(result3[1])
      expectType<string | undefined>(result3[0].data)
      expectType<number | undefined>(result3[1].data)

      // types should be enforced
      useQueries<
        [
          { queryFnData: string; data: string },
          { queryFnData: string; data: number; error: boolean },
        ]
      >({
        queries: [
          {
            queryKey: key1,
            queryFn: () => 'string',
            select: (a) => {
              expectType<string>(a)
              expectTypeNotAny(a)
              return a.toLowerCase()
            },
            onSuccess: (a) => {
              expectType<string>(a)
              expectTypeNotAny(a)
            },
            placeholderData: 'string',
            // @ts-expect-error (initialData: string)
            initialData: 123,
          },
          {
            queryKey: key2,
            queryFn: () => 'string',
            select: (a) => {
              expectType<string>(a)
              expectTypeNotAny(a)
              return parseInt(a)
            },
            onSuccess: (a) => {
              expectType<number>(a)
              expectTypeNotAny(a)
            },
            onError: (e) => {
              expectType<boolean>(e)
              expectTypeNotAny(e)
            },
            placeholderData: 'string',
            // @ts-expect-error (initialData: string)
            initialData: 123,
          },
        ],
      })

      // field names should be enforced
      useQueries<[{ queryFnData: string }]>({
        queries: [
          {
            queryKey: key1,
            queryFn: () => 'string',
            // @ts-expect-error (invalidField)
            someInvalidField: [],
          },
        ],
      })
    }
  })

  it('handles array literal without type parameter to infer result type', async () => {
    const key1 = queryKey()
    const key2 = queryKey()
    const key3 = queryKey()
    const key4 = queryKey()

    // @ts-expect-error (Page component is not rendered)
    // eslint-disable-next-line
    function Page() {
      // Array.map preserves TQueryFnData
      const result1 = useQueries({
        queries: Array(50).map((_, i) => ({
          queryKey: ['key', i] as const,
          queryFn: () => i + 10,
        })),
      })
      expectType<QueryObserverResult<number, unknown>[]>(result1)
      expectType<number | undefined>(result1[0]?.data)

      // Array.map preserves TData
      const result2 = useQueries({
        queries: Array(50).map((_, i) => ({
          queryKey: ['key', i] as const,
          queryFn: () => i + 10,
          select: (data: number) => data.toString(),
        })),
      })
      expectType<QueryObserverResult<string, unknown>[]>(result2)

      const result3 = useQueries({
        queries: [
          {
            queryKey: key1,
            queryFn: () => 1,
          },
          {
            queryKey: key2,
            queryFn: () => 'string',
          },
          {
            queryKey: key3,
            queryFn: () => ['string[]'],
            select: () => 123,
          },
        ],
      })
      expectType<QueryObserverResult<number, unknown>>(result3[0])
      expectType<QueryObserverResult<string, unknown>>(result3[1])
      expectType<QueryObserverResult<number, unknown>>(result3[2])
      expectType<number | undefined>(result3[0].data)
      expectType<string | undefined>(result3[1].data)
      // select takes precedence over queryFn
      expectType<number | undefined>(result3[2].data)

      // initialData/placeholderData are enforced
      useQueries({
        queries: [
          {
            queryKey: key1,
            queryFn: () => 'string',
            placeholderData: 'string',
            // @ts-expect-error (initialData: string)
            initialData: 123,
          },
          {
            queryKey: key2,
            queryFn: () => 123,
            // @ts-expect-error (placeholderData: number)
            placeholderData: 'string',
            initialData: 123,
          },
        ],
      })

      // select / onSuccess / onSettled params are "indirectly" enforced
      useQueries({
        queries: [
          // unfortunately TS will not suggest the type for you
          {
            queryKey: key1,
            queryFn: () => 'string',
            // @ts-expect-error (noImplicitAny)
            onSuccess: (a) => null,
            // @ts-expect-error (noImplicitAny)
            onSettled: (a) => null,
          },
          // however you can add a type to the callback
          {
            queryKey: key2,
            queryFn: () => 'string',
            onSuccess: (a: string) => {
              expectType<string>(a)
              expectTypeNotAny(a)
            },
            onSettled: (a: string | undefined) => {
              expectType<string | undefined>(a)
              expectTypeNotAny(a)
            },
          },
          // the type you do pass is enforced
          {
            queryKey: key3,
            queryFn: () => 'string',
            // @ts-expect-error (only accepts string)
            onSuccess: (a: number) => null,
          },
          {
            queryKey: key4,
            queryFn: () => 'string',
            select: (a: string) => parseInt(a),
            // @ts-expect-error (select is defined => only accepts number)
            onSuccess: (a: string) => null,
            onSettled: (a: number | undefined) => {
              expectType<number | undefined>(a)
              expectTypeNotAny(a)
            },
          },
        ],
      })

      // callbacks are also indirectly enforced with Array.map
      useQueries({
        // @ts-expect-error (onSuccess only accepts string)
        queries: Array(50).map((_, i) => ({
          queryKey: ['key', i] as const,
          queryFn: () => i + 10,
          select: (data: number) => data.toString(),
          onSuccess: (_data: number) => null,
        })),
      })
      useQueries({
        queries: Array(50).map((_, i) => ({
          queryKey: ['key', i] as const,
          queryFn: () => i + 10,
          select: (data: number) => data.toString(),
          onSuccess: (_data: string) => null,
        })),
      })

      // results inference works when all the handlers are defined
      const result4 = useQueries({
        queries: [
          {
            queryKey: key1,
            queryFn: () => 'string',
            // @ts-expect-error (noImplicitAny)
            onSuccess: (a) => null,
            // @ts-expect-error (noImplicitAny)
            onSettled: (a) => null,
          },
          {
            queryKey: key2,
            queryFn: () => 'string',
            onSuccess: (a: string) => {
              expectType<string>(a)
              expectTypeNotAny(a)
            },
            onSettled: (a: string | undefined) => {
              expectType<string | undefined>(a)
              expectTypeNotAny(a)
            },
          },
          {
            queryKey: key4,
            queryFn: () => 'string',
            select: (a: string) => parseInt(a),
            onSuccess: (_a: number) => null,
            onSettled: (a: number | undefined) => {
              expectType<number | undefined>(a)
              expectTypeNotAny(a)
            },
          },
        ],
      })
      expectType<QueryObserverResult<string, unknown>>(result4[0])
      expectType<QueryObserverResult<string, unknown>>(result4[1])
      expectType<QueryObserverResult<number, unknown>>(result4[2])

      // handles when queryFn returns a Promise
      const result5 = useQueries({
        queries: [
          {
            queryKey: key1,
            queryFn: () => Promise.resolve('string'),
            onSuccess: (a: string) => {
              expectType<string>(a)
              expectTypeNotAny(a)
            },
            // @ts-expect-error (refuses to accept a Promise)
            onSettled: (a: Promise<string>) => null,
          },
        ],
      })
      expectType<QueryObserverResult<string, unknown>>(result5[0])

      // Array as const does not throw error
      const result6 = useQueries({
        queries: [
          {
            queryKey: ['key1'],
            queryFn: () => 'string',
          },
          {
            queryKey: ['key1'],
            queryFn: () => 123,
          },
        ],
      } as const)
      expectType<QueryObserverResult<string, unknown>>(result6[0])
      expectType<QueryObserverResult<number, unknown>>(result6[1])

      // field names should be enforced - array literal
      useQueries({
        queries: [
          {
            queryKey: key1,
            queryFn: () => 'string',
            // @ts-expect-error (invalidField)
            someInvalidField: [],
          },
        ],
      })

      // field names should be enforced - Array.map() result
      useQueries({
        // @ts-expect-error (invalidField)
        queries: Array(10).map(() => ({
          someInvalidField: '',
        })),
      })

      // field names should be enforced - array literal
      useQueries({
        queries: [
          {
            queryKey: key1,
            queryFn: () => 'string',
            // @ts-expect-error (invalidField)
            someInvalidField: [],
          },
        ],
      })

      // supports queryFn using fetch() to return Promise<any> - Array.map() result
      useQueries({
        queries: Array(50).map((_, i) => ({
          queryKey: ['key', i] as const,
          queryFn: () =>
            fetch('return Promise<any>').then((resp) => resp.json()),
        })),
      })

      // supports queryFn using fetch() to return Promise<any> - array literal
      useQueries({
        queries: [
          {
            queryKey: key1,
            queryFn: () =>
              fetch('return Promise<any>').then((resp) => resp.json()),
          },
        ],
      })
    }
  })

  it('handles strongly typed queryFn factories and useQueries wrappers', () => {
    // QueryKey + queryFn factory
    type QueryKeyA = ['queryA']
    const getQueryKeyA = (): QueryKeyA => ['queryA']
    type GetQueryFunctionA = () => QueryFunction<number, QueryKeyA>
    const getQueryFunctionA: GetQueryFunctionA = () => async () => {
      return 1
    }
    type SelectorA = (data: number) => [number, string]
    const getSelectorA = (): SelectorA => (data) => [data, data.toString()]

    type QueryKeyB = ['queryB', string]
    const getQueryKeyB = (id: string): QueryKeyB => ['queryB', id]
    type GetQueryFunctionB = () => QueryFunction<string, QueryKeyB>
    const getQueryFunctionB: GetQueryFunctionB = () => async () => {
      return '1'
    }
    type SelectorB = (data: string) => [string, number]
    const getSelectorB = (): SelectorB => (data) => [data, +data]

    // Wrapper with strongly typed array-parameter
    function useWrappedQueries<
      TQueryFnData,
      TError,
      TData,
      TQueryKey extends QueryKey,
    >(queries: UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>[]) {
      return useQueries({
        queries: queries.map(
          // no need to type the mapped query
          (query) => {
            const { queryFn: fn, queryKey: key, onError: err } = query
            expectType<QueryFunction<TQueryFnData, TQueryKey> | undefined>(fn)
            return {
              queryKey: key,
              onError: err,
              queryFn: fn
                ? (ctx: QueryFunctionContext<TQueryKey>) => {
                    expectType<TQueryKey>(ctx.queryKey)
                    return fn.call({}, ctx)
                  }
                : undefined,
            }
          },
        ),
      })
    }

    // @ts-expect-error (Page component is not rendered)
    // eslint-disable-next-line
    function Page() {
      const result = useQueries({
        queries: [
          {
            queryKey: getQueryKeyA(),
            queryFn: getQueryFunctionA(),
          },
          {
            queryKey: getQueryKeyB('id'),
            queryFn: getQueryFunctionB(),
          },
        ],
      })
      expectType<QueryObserverResult<number, unknown>>(result[0])
      expectType<QueryObserverResult<string, unknown>>(result[1])

      const withSelector = useQueries({
        queries: [
          {
            queryKey: getQueryKeyA(),
            queryFn: getQueryFunctionA(),
            select: getSelectorA(),
          },
          {
            queryKey: getQueryKeyB('id'),
            queryFn: getQueryFunctionB(),
            select: getSelectorB(),
          },
        ],
      })
      expectType<QueryObserverResult<[number, string], unknown>>(
        withSelector[0],
      )
      expectType<QueryObserverResult<[string, number], unknown>>(
        withSelector[1],
      )

      const withWrappedQueries = useWrappedQueries(
        Array(10).map(() => ({
          queryKey: getQueryKeyA(),
          queryFn: getQueryFunctionA(),
          select: getSelectorA(),
        })),
      )

      expectType<QueryObserverResult<number | undefined, unknown>[]>(
        withWrappedQueries,
      )
    }
  })

  it('should not change state if unmounted', async () => {
    const key1 = queryKey()

    // We have to mock the QueriesObserver to not unsubscribe
    // the listener when the component is unmounted
    class QueriesObserverMock extends QueriesObserver {
      subscribe(listener: any) {
        super.subscribe(listener)
        return () => void 0
      }
    }

    const QueriesObserverSpy = jest
      .spyOn(QueriesObserverModule, 'QueriesObserver')
      .mockImplementation((fn) => {
        return new QueriesObserverMock(fn)
      })

    function Queries() {
      useQueries({
        queries: [
          {
            queryKey: key1,
            queryFn: async () => {
              await sleep(10)
              return 1
            },
          },
        ],
      })

      return (

          queries

      )
    }

    function Page() {
      const [mounted, setMounted] = React.useState(true)

      return (

          <button onClick={() => setMounted(false)}>unmount</button>
          {mounted && <Queries />}

      )
    }

    const { getByText } = renderWithClient(queryClient, <Page />)
    fireEvent.click(getByText('unmount'))

    // Should not display the console error
    // "Warning: Can't perform a React state update on an unmounted component"

    await sleep(20)
    QueriesObserverSpy.mockRestore()
  })

  describe('with custom context', () => {
    it('should return the correct states', async () => {
      const context = React.createContext<QueryClient | undefined>(undefined)

      const key1 = queryKey()
      const key2 = queryKey()
      const results: UseQueryResult[][] = []

      function Page() {
        const result = useQueries({
          context,
          queries: [
            {
              queryKey: key1,
              queryFn: async () => {
                await sleep(5)
                return 1
              },
            },
            {
              queryKey: key2,
              queryFn: async () => {
                await sleep(10)
                return 2
              },
            },
          ],
        })
        results.push(result)
        return null
      }

      renderWithClient(queryClient, <Page />, { context })

      await sleep(30)

      expect(results[0]).toMatchObject([
        { data: undefined },
        { data: undefined },
      ])
      expect(results[results.length - 1]).toMatchObject([
        { data: 1 },
        { data: 2 },
      ])
    })

    it('should throw if the context is necessary and is not passed to useQueries', async () => {
      const context = React.createContext<QueryClient | undefined>(undefined)

      const key1 = queryKey()
      const key2 = queryKey()
      const results: UseQueryResult[][] = []

      function Page() {
        const result = useQueries({
          queries: [
            {
              queryKey: key1,
              queryFn: async () => 1,
            },
            {
              queryKey: key2,
              queryFn: async () => 2,
            },
          ],
        })
        results.push(result)
        return null
      }

      const rendered = renderWithClient(
        queryClient,
        <ErrorBoundary fallbackRender={() =>
error boundary
}>
          <Page />
        </ErrorBoundary>,
        { context },
      )

      await waitFor(() => rendered.getByText('error boundary'))
    })
  })
})


{
  "extends": ["react-app", "prettier"],
  "rules": {
    // "eqeqeq": 0,
    // "jsx-a11y/anchor-is-valid": 0
  }
}


# Example

To run this example:

- `npm install`
- `npm run dev`


keep keep keep keep replace keep keep keep

@tanstack/query-sync-storage-persister

TODO


import React from "react";
import { Typography } from "@material-ui/core";
import { makeStyles } from "@material-ui/core/styles";
import { Link } from "@material-ui/core";
import { Link as RouterLink } from "react-router-dom";

export default function Home() {
  const classes = useStyles();

  return (

      <Typography variant="h2">React Query Demo</Typography>
      <Typography variant="subtitle1">Using the Rick And Morty API</Typography>
      <Typography variant="subtitle2">
        (Built by @axelfuh
        )
      </Typography>

        <Typography variant="h5">Why React Query?</Typography>
        <Typography variant="body1">
          In this demo you will be able to see how React Query is a significant
          improvement over redux, mobx, and
          any other general-purpose state container.
        </Typography>
        <Typography variant="body1">
          No reducers, thunks, or sagas. No ES6 models to maintain in order to
          tag them as observable.
        </Typography>
        <Typography variant="body1">
          Simply associate a key with your fetch call and let{" "}
          React Query handle the rest.
        </Typography>
        <Typography variant="h5">Ready to get started?</Typography>
        <Typography variant="body1">
          Check out the{" "}
          <Link component={RouterLink} to="/episodes">
            Episodes
          </Link>{" "}
          and{" "}
          <Link component={RouterLink} to="/characters">
            Characters
          </Link>
          !
        </Typography>

  );
}

const useStyles = makeStyles(() => ({
  main: {
    margin: "44px 0",
    "& p": {
      margin: "12px 0 24px"
    }
  }
}));