import { expect } from 'chai';
import proxyquire from 'proxyquire';
import sinon from 'sinon';

import { RoomMemberActions, RoomSettingsEnum } from '../../../../../definition/IRoomTypeConfig';

const findOneStub = sinon.stub();

const Federation = proxyquire.noCallThru().load('../../../../../client/lib/federation/Federation', {
	'../../../app/models/client': {
		RoomRoles: {
			findOne: findOneStub,
		},
	},
});

describe('Federation[Client] - Federation', () => {
	afterEach(() => findOneStub.reset());

	describe('#actionAllowed()', () => {
		const me = 'user-id';
		const them = 'other-user-id';

		it('should return false if the room is not federated', () => {
			expect(Federation.actionAllowed({ federated: false } as any, RoomMemberActions.REMOVE_USER, 'user-id', { roles: ['owner'] } as any))
				.to.be.false;
		});

		it('should return false if the room is a direct message', () => {
			expect(
				Federation.actionAllowed({ federated: true, t: 'd' } as any, RoomMemberActions.REMOVE_USER, 'user-id', { roles: ['owner'] } as any),
			).to.be.false;
		});

		it('should return false if the user is not subscribed to the room', () => {
			expect(Federation.actionAllowed({ federated: true } as any, RoomMemberActions.REMOVE_USER, 'user-id', undefined)).to.be.false;
		});

		it('should return false if the user is trying to remove himself', () => {
			expect(
				Federation.actionAllowed({ federated: true } as any, RoomMemberActions.REMOVE_USER, 'user-id', {
					u: { _id: 'user-id' },
					roles: ['owner'],
				} as any),
			).to.be.false;
		});

		describe('Owners', () => {
			const myRole = ['owner'];

			describe('Seeing another owners', () => {
				const theirRole = ['owner'];
				it('should return true if the user want to remove himself as an owner', () => {
					findOneStub.returns({ roles: theirRole });
					expect(
						Federation.actionAllowed({ federated: true } as any, RoomMemberActions.SET_AS_OWNER, me, {
							u: { _id: me },
							roles: myRole,
						} as any),
					).to.be.true;
				});
				it('should return true if the user want to add himself as a moderator (Demoting himself to moderator)', () => {
					findOneStub.returns({ roles: theirRole });
					expect(
						Federation.actionAllowed({ federated: true } as any, RoomMemberActions.SET_AS_MODERATOR, me, {
							u: { _id: me },
							roles: myRole,
						} as any),
					).to.be.true;
				});
				it('should return false if the user want to remove another owners as an owner', () => {
					findOneStub.returns({ roles: theirRole });
					expect(
						Federation.actionAllowed({ federated: true } as any, RoomMemberActions.SET_AS_OWNER, me, {
							u: { _id: them },
							roles: myRole,
						} as any),
					).to.be.false;
				});
				it('should return false if the user want to remove another owners from the room', () => {
					findOneStub.returns({ roles: theirRole });
					expect(
						Federation.actionAllowed({ federated: true } as any, RoomMemberActions.REMOVE_USER, me, {
							u: { _id: them },
							roles: myRole,
						} as any),
					).to.be.false;
				});
			});
			describe('Seeing moderators', () => {
				const theirRole = ['moderator'];

				it('should return true if the user want to add/remove moderators as an owner', () => {
					findOneStub.returns({ roles: theirRole });
					expect(
						Federation.actionAllowed({ federated: true } as any, RoomMemberActions.SET_AS_OWNER, me, {
							u: { _id: them },
							roles: myRole,
						} as any),
					).to.be.true;
				});
				it('should return true if the user want to remove moderators as a moderator', () => {
					findOneStub.returns({ roles: theirRole });
					expect(
						Federation.actionAllowed({ federated: true } as any, RoomMemberActions.SET_AS_MODERATOR, me, {
							u: { _id: them },
							roles: myRole,
						} as any),
					).to.be.true;
				});
				it('should return true if the user want to remove moderators from the room', () => {
					findOneStub.returns({ roles: theirRole });
					expect(
						Federation.actionAllowed({ federated: true } as any, RoomMemberActions.REMOVE_USER, me, {
							u: { _id: them },
							roles: myRole,
						} as any),
					).to.be.true;
				});
			});
			describe('Seeing normal users', () => {
				it('should return true if the user want to add/remove normal users as an owner', () => {
					findOneStub.returns(undefined);
					expect(
						Federation.actionAllowed({ federated: true } as any, RoomMemberActions.SET_AS_OWNER, me, {
							u: { _id: them },
							roles: myRole,
						} as any),
					).to.be.true;
				});
				it('should return true if the user want to add/remove normal users as a moderator', () => {
					findOneStub.returns(undefined);
					expect(
						Federation.actionAllowed({ federated: true } as any, RoomMemberActions.SET_AS_MODERATOR, me, {
							u: { _id: them },
							roles: myRole,
						} as any),
					).to.be.true;
				});
				it('should return true if the user want to remove normal users from the room', () => {
					findOneStub.returns(undefined);
					expect(
						Federation.actionAllowed({ federated: true } as any, RoomMemberActions.REMOVE_USER, me, {
							u: { _id: them },
							roles: myRole,
						} as any),
					).to.be.true;
				});
			});
		});

		describe('Moderators', () => {
			const myRole = ['moderator'];
			describe('Seeing owners', () => {
				const theirRole = ['owner'];
				it('should return false if the user want to add/remove owners as a moderator', () => {
					findOneStub.returns({ roles: theirRole });
					expect(
						Federation.actionAllowed({ federated: true } as any, RoomMemberActions.SET_AS_OWNER, me, {
							u: { _id: me },
							roles: myRole,
						} as any),
					).to.be.false;
				});
				it('should return false if the user want to add/remove owners as a moderator', () => {
					findOneStub.returns({ roles: theirRole });
					expect(
						Federation.actionAllowed({ federated: true } as any, RoomMemberActions.SET_AS_MODERATOR, me, {
							u: { _id: me },
							roles: myRole,
						} as any),
					).to.be.false;
				});
				it('should return false if the user want to add/remove owners as a moderator', () => {
					findOneStub.returns({ roles: theirRole });
					expect(
						Federation.actionAllowed({ federated: true } as any, RoomMemberActions.SET_AS_MODERATOR, me, {
							u: { _id: me },
							roles: myRole,
						} as any),
					).to.be.false;
				});
				it('should return false if the user want to remove owners from the room', () => {
					findOneStub.returns({ roles: theirRole });
					expect(
						Federation.actionAllowed({ federated: true } as any, RoomMemberActions.REMOVE_USER, me, {
							u: { _id: me },
							roles: myRole,
						} as any),
					).to.be.false;
				});
			});
			describe('Seeing another moderators', () => {
				const theirRole = ['moderator'];
				it('should return false if the user want to add/remove moderator as an owner', () => {
					findOneStub.returns({ roles: theirRole });
					expect(
						Federation.actionAllowed({ federated: true } as any, RoomMemberActions.SET_AS_OWNER, me, {
							u: { _id: them },
							roles: myRole,
						} as any),
					).to.be.false;
				});
				it('should return true if the user want to remove himself as a moderator (Demoting himself)', () => {
					findOneStub.returns({ roles: theirRole });
					expect(
						Federation.actionAllowed({ federated: true } as any, RoomMemberActions.SET_AS_MODERATOR, me, {
							u: { _id: me },
							roles: myRole,
						} as any),
					).to.be.true;
				});
				it('should return false if the user want to promote himself as an owner', () => {
					findOneStub.returns({ roles: theirRole });
					expect(
						Federation.actionAllowed({ federated: true } as any, RoomMemberActions.SET_AS_OWNER, me, {
							u: { _id: me },
							roles: myRole,
						} as any),
					).to.be.false;
				});
				it('should return false if the user want to remove another moderator from their role', () => {
					findOneStub.returns({ roles: theirRole });
					expect(
						Federation.actionAllowed({ federated: true } as any, RoomMemberActions.SET_AS_MODERATOR, me, {
							u: { _id: them },
							roles: myRole,
						} as any),
					).to.be.false;
				});
				it('should return false if the user want to remove another moderator from the room', () => {
					findOneStub.returns({ roles: theirRole });
					expect(
						Federation.actionAllowed({ federated: true } as any, RoomMemberActions.REMOVE_USER, me, {
							u: { _id: them },
							roles: myRole,
						} as any),
					).to.be.false;
				});
			});
			describe('Seeing normal users', () => {
				it('should return false if the user want to add/remove normal users as an owner', () => {
					findOneStub.returns(undefined);
					expect(
						Federation.actionAllowed({ federated: true } as any, RoomMemberActions.SET_AS_OWNER, me, {
							u: { _id: them },
							roles: myRole,
						} as any),
					).to.be.false;
				});
				it('should return true if the user want to add/remove normal users as a moderator', () => {
					findOneStub.returns(undefined);
					expect(
						Federation.actionAllowed({ federated: true } as any, RoomMemberActions.SET_AS_MODERATOR, me, {
							u: { _id: them },
							roles: myRole,
						} as any),
					).to.be.true;
				});
				it('should return true if the user want to remove normal users from the room', () => {
					findOneStub.returns(undefined);
					expect(
						Federation.actionAllowed({ federated: true } as any, RoomMemberActions.REMOVE_USER, me, {
							u: { _id: them },
							roles: myRole,
						} as any),
					).to.be.true;
				});
			});
		});

		describe('Normal user', () => {
			describe('Seeing owners', () => {
				const theirRole = ['owner'];
				it('should return false if the user want to add/remove owners as a normal user', () => {
					findOneStub.returns({ roles: theirRole });
					expect(
						Federation.actionAllowed({ federated: true } as any, RoomMemberActions.SET_AS_OWNER, me, {
							u: { _id: them },
						} as any),
					).to.be.false;
				});
				it('should return false if the user want to add/remove moderators as a normal user', () => {
					findOneStub.returns({ roles: theirRole });
					expect(
						Federation.actionAllowed({ federated: true } as any, RoomMemberActions.SET_AS_MODERATOR, me, {
							u: { _id: them },
						} as any),
					).to.be.false;
				});
				it('should return false if the user want to remove owners from the room', () => {
					findOneStub.returns({ roles: theirRole });
					expect(
						Federation.actionAllowed({ federated: true } as any, RoomMemberActions.REMOVE_USER, me, {
							u: { _id: them },
						} as any),
					).to.be.false;
				});
			});
			describe('Seeing moderators', () => {
				const theirRole = ['owner'];
				it('should return false if the user want to add/remove owner as a normal user', () => {
					findOneStub.returns({ roles: theirRole });
					expect(
						Federation.actionAllowed({ federated: true } as any, RoomMemberActions.SET_AS_OWNER, me, {
							u: { _id: them },
						} as any),
					).to.be.false;
				});
				it('should return false if the user want to remove a moderator from their role', () => {
					findOneStub.returns({ roles: theirRole });
					expect(
						Federation.actionAllowed({ federated: true } as any, RoomMemberActions.SET_AS_MODERATOR, me, {
							u: { _id: them },
						} as any),
					).to.be.false;
				});
				it('should return false if the user want to remove a moderator from the room', () => {
					findOneStub.returns({ roles: theirRole });
					expect(
						Federation.actionAllowed({ federated: true } as any, RoomMemberActions.REMOVE_USER, me, {
							u: { _id: them },
						} as any),
					).to.be.false;
				});
			});
			describe('Seeing another normal users', () => {
				it('should return false if the user want to add/remove owner as a normal user', () => {
					findOneStub.returns(undefined);
					expect(
						Federation.actionAllowed({ federated: true } as any, RoomMemberActions.SET_AS_OWNER, me, {
							u: { _id: them },
						} as any),
					).to.be.false;
				});
				it('should return false if the user want to add/remove moderator as a normal user', () => {
					findOneStub.returns(undefined);
					expect(
						Federation.actionAllowed({ federated: true } as any, RoomMemberActions.SET_AS_OWNER, me, {
							u: { _id: them },
						} as any),
					).to.be.false;
				});
				it('should return false if the user want to remove normal users from the room', () => {
					findOneStub.returns(undefined);
					expect(
						Federation.actionAllowed({ federated: true } as any, RoomMemberActions.REMOVE_USER, me, {
							u: { _id: them },
						} as any),
					).to.be.false;
				});

				[RoomMemberActions.SET_AS_MODERATOR, RoomMemberActions.SET_AS_OWNER, RoomMemberActions.REMOVE_USER].forEach((action) => {
					it(`should return false if the user want to ${action} for himself`, () => {
						findOneStub.returns(undefined);
						expect(
							Federation.actionAllowed({ federated: true } as any, action, me, {
								u: { _id: me },
							} as any),
						).to.be.false;
					});
				});
			});
		});
	});

	describe('#isEditableByTheUser()', () => {
		it('should return false if the user is null', () => {
			expect(Federation.isEditableByTheUser(undefined, { u: { _id: 'id' } } as any, {} as any)).to.be.false;
		});

		it('should return false if the room is null', () => {
			expect(Federation.isEditableByTheUser({} as any, undefined, {} as any)).to.be.false;
		});

		it('should return false if the current user is NOT the room owner', () => {
			expect(Federation.isEditableByTheUser({ _id: 'differentId' } as any, { u: { _id: 'id' } } as any, {} as any)).to.be.false;
		});
	});

	describe('#isRoomSettingAllowed()', () => {
		it('should return false if the room is NOT federated', () => {
			expect(Federation.isRoomSettingAllowed({ t: 'c' } as any, RoomSettingsEnum.NAME)).to.be.false;
		});

		it('should return false if the room is a DM one', () => {
			expect(Federation.isRoomSettingAllowed({ t: 'd', federated: true } as any, RoomSettingsEnum.NAME)).to.be.false;
		});

		const allowedSettingsChanges = [RoomSettingsEnum.NAME, RoomSettingsEnum.TOPIC];

		Object.values(RoomSettingsEnum)
			.filter((setting) => !allowedSettingsChanges.includes(setting as any))
			.forEach((setting) => {
				it('should return false if the setting change is NOT allowed within the federation context for regular channels', () => {
					expect(Federation.isRoomSettingAllowed({ t: 'c', federated: true } as any, setting)).to.be.false;
				});
			});

		allowedSettingsChanges.forEach((setting) => {
			it('should return true if the setting change is allowed within the federation context for regular channels', () => {
				expect(Federation.isRoomSettingAllowed({ t: 'c', federated: true } as any, setting)).to.be.true;
			});
		});
	});
});


expect(states[0]).toMatchObject({ data: undefined })

expect(states[1]).toMatchObject({ data: 1 })

expect(states[2]).toMatchObject({ data: 1 })

expect(states[3]).toMatchObject({ data: undefined })

keep keep keep keep replace keep keep keep

expect(states[2]).toMatchObject({ data: undefined })

import { waitFor, fireEvent } from '@testing-library/react'
import React from 'react'

import {
  queryKey,
  sleep,
  mockConsoleError,
  renderWithClient,
  setActTimeout,
  Blink,
} from './utils'
import {
  useInfiniteQuery,
  UseInfiniteQueryResult,
  QueryClient,
  QueryCache,
} from '../..'

interface Result {
  items: number[]
  nextId?: number
  prevId?: number
  ts: number
}

const pageSize = 10

const fetchItems = async (
  page: number,
  ts: number,
  noNext?: boolean,
  noPrev?: boolean
): Promise<Result> => {
  await sleep(10)
  return {
    items: [...new Array(10)].fill(null).map((_, d) => page * pageSize + d),
    nextId: noNext ? undefined : page + 1,
    prevId: noPrev ? undefined : page - 1,
    ts,
  }
}

describe('useInfiniteQuery', () => {
  const queryCache = new QueryCache()
  const queryClient = new QueryClient({ queryCache })

  it('should return the correct states for a successful query', async () => {
    const key = queryKey()
    const states: UseInfiniteQueryResult<number>[] = []

    function Page() {
      const state = useInfiniteQuery(
        key,
        ({ pageParam = 0 }) => Number(pageParam),
        {
          getNextPageParam: lastPage => lastPage + 1,
        }
      )
      states.push(state)
      return null
    }

    renderWithClient(queryClient, <Page />)

    await sleep(100)

    expect(states.length).toBe(2)
    expect(states[0]).toEqual({
      data: undefined,
      dataUpdatedAt: 0,
      error: null,
      errorUpdatedAt: 0,
      failureCount: 0,
      fetchNextPage: expect.any(Function),
      fetchPreviousPage: expect.any(Function),
      hasNextPage: undefined,
      hasPreviousPage: undefined,
      isError: false,
      isFetched: false,
      isFetchedAfterMount: false,
      isFetching: true,
      isFetchingNextPage: false,
      isFetchingPreviousPage: false,
      isIdle: false,
      isLoading: true,
      isLoadingError: false,
      isPlaceholderData: false,
      isPreviousData: false,
      isRefetchError: false,
      isStale: true,
      isSuccess: false,
      refetch: expect.any(Function),
      remove: expect.any(Function),
      status: 'loading',
    })

    expect(states[1]).toEqual({
      data: { pages: [0], pageParams: [undefined] },
      dataUpdatedAt: expect.any(Number),
      error: null,
      errorUpdatedAt: 0,
      failureCount: 0,
      fetchNextPage: expect.any(Function),
      fetchPreviousPage: expect.any(Function),
      hasNextPage: true,
      hasPreviousPage: undefined,
      isError: false,
      isFetched: true,
      isFetchedAfterMount: true,
      isFetching: false,
      isFetchingNextPage: false,
      isFetchingPreviousPage: false,
      isIdle: false,
      isLoading: false,
      isLoadingError: false,
      isPlaceholderData: false,
      isPreviousData: false,
      isRefetchError: false,
      isStale: true,
      isSuccess: true,
      refetch: expect.any(Function),
      remove: expect.any(Function),
      status: 'success',
    })
  })

  it('should not throw when fetchNextPage returns an error', async () => {
    const consoleMock = mockConsoleError()
    const key = queryKey()
    let noThrow: boolean

    function Page() {
      const start = 1
      const state = useInfiniteQuery(
        key,
        async ({ pageParam = start }) => {
          if (pageParam === 2) {
            throw new Error('error')
          }
          return Number(pageParam)
        },
        {
          retry: 1,
          retryDelay: 10,
          getNextPageParam: lastPage => lastPage + 1,
        }
      )

      const { fetchNextPage } = state

      React.useEffect(() => {
        setActTimeout(() => {
          fetchNextPage()
            .then(() => {
              noThrow = true
            })
            .catch(() => undefined)
        }, 20)
      }, [fetchNextPage])

      return null
    }

    renderWithClient(queryClient, <Page />)

    await waitFor(() => expect(noThrow).toBe(true))
    consoleMock.mockRestore()
  })

  it('should keep the previous data when keepPreviousData is set', async () => {
    const key = queryKey()
    const states: UseInfiniteQueryResult<string>[] = []

    function Page() {
      const [order, setOrder] = React.useState('desc')

      const state = useInfiniteQuery(
        [key, order],
        async ({ pageParam = 0 }) => {
          await sleep(10)
          return `${pageParam}-${order}`
        },
        {
          getNextPageParam: () => 1,
          keepPreviousData: true,
        }
      )

      states.push(state)

      const { fetchNextPage } = state

      React.useEffect(() => {
        setActTimeout(() => {
          fetchNextPage()
        }, 50)
        setActTimeout(() => {
          setOrder('asc')
        }, 100)
      }, [fetchNextPage])

      return null
    }

    renderWithClient(queryClient, <Page />)

    await sleep(300)

    expect(states.length).toBe(7)
    expect(states[0]).toMatchObject({
      data: undefined,
      isFetching: true,
      isFetchingNextPage: false,
      isSuccess: false,
      isPreviousData: false,
    })
    expect(states[1]).toMatchObject({
      data: { pages: ['0-desc'] },
      isFetching: false,
      isFetchingNextPage: false,
      isSuccess: true,
      isPreviousData: false,
    })
    expect(states[2]).toMatchObject({
      data: { pages: ['0-desc'] },
      isFetching: true,
      isFetchingNextPage: true,
      isSuccess: true,
      isPreviousData: false,
    })
    expect(states[3]).toMatchObject({
      data: { pages: ['0-desc', '1-desc'] },
      isFetching: false,
      isFetchingNextPage: false,
      isSuccess: true,
      isPreviousData: false,
    })
    // Set state
    expect(states[4]).toMatchObject({
      data: { pages: ['0-desc', '1-desc'] },
      isFetching: false,
      isFetchingNextPage: false,
      isSuccess: true,
      isPreviousData: false,
    })
    expect(states[5]).toMatchObject({
      data: { pages: ['0-desc', '1-desc'] },
      isFetching: true,
      isFetchingNextPage: false,
      isSuccess: true,
      isPreviousData: true,
    })
    expect(states[6]).toMatchObject({
      data: { pages: ['0-asc'] },
      isFetching: false,
      isFetchingNextPage: false,
      isSuccess: true,
      isPreviousData: false,
    })
  })

  it('should be able to select a part of the data', async () => {
    const key = queryKey()
    const states: UseInfiniteQueryResult<string>[] = []

    function Page() {
      const state = useInfiniteQuery(key, () => ({ count: 1 }), {
        select: data => ({
          pages: data.pages.map(x => `count: ${x.count}`),
          pageParams: data.pageParams,
        }),
      })
      states.push(state)
      return null
    }

    renderWithClient(queryClient, <Page />)

    await sleep(10)

    expect(states.length).toBe(2)
    expect(states[0]).toMatchObject({
      data: undefined,
      isSuccess: false,
    })
    expect(states[1]).toMatchObject({
      data: { pages: ['count: 1'] },
      isSuccess: true,
    })
  })

  it('should be able to reverse the data', async () => {
    const key = queryKey()
    const states: UseInfiniteQueryResult<number>[] = []

    function Page() {
      const state = useInfiniteQuery(
        key,
        ({ pageParam = 0 }) => Number(pageParam),
        {
          select: data => ({
            pages: [...data.pages].reverse(),
            pageParams: [...data.pageParams].reverse(),
          }),
        }
      )

      states.push(state)

      const { fetchNextPage } = state

      React.useEffect(() => {
        setActTimeout(() => {
          fetchNextPage({ pageParam: 1 })
        }, 10)
      }, [fetchNextPage])

      return null
    }

    renderWithClient(queryClient, <Page />)

    await sleep(100)

    expect(states.length).toBe(4)
    expect(states[0]).toMatchObject({
      data: undefined,
      isSuccess: false,
    })
    expect(states[1]).toMatchObject({
      data: { pages: [0] },
      isSuccess: true,
    })
    expect(states[2]).toMatchObject({
      data: { pages: [0] },
      isSuccess: true,
    })
    expect(states[3]).toMatchObject({
      data: { pages: [1, 0] },
      isSuccess: true,
    })
  })

  it('should be able to fetch a previous page', async () => {
    const key = queryKey()
    const states: UseInfiniteQueryResult<number>[] = []

    function Page() {
      const start = 10
      const state = useInfiniteQuery(
        key,
        async ({ pageParam = start }) => {
          await sleep(10)
          return Number(pageParam)
        },
        {
          getPreviousPageParam: firstPage => firstPage - 1,
        }
      )

      states.push(state)

      const { fetchPreviousPage } = state

      React.useEffect(() => {
        setActTimeout(() => {
          fetchPreviousPage()
        }, 20)
      }, [fetchPreviousPage])

      return null
    }

    renderWithClient(queryClient, <Page />)

    await sleep(100)

    expect(states.length).toBe(4)
    expect(states[0]).toMatchObject({
      data: undefined,
      hasNextPage: undefined,
      hasPreviousPage: undefined,
      isFetching: true,
      isFetchingNextPage: false,
      isFetchingPreviousPage: false,
      isSuccess: false,
    })
    expect(states[1]).toMatchObject({
      data: { pages: [10] },
      hasNextPage: undefined,
      hasPreviousPage: true,
      isFetching: false,
      isFetchingNextPage: false,
      isFetchingPreviousPage: false,
      isSuccess: true,
    })
    expect(states[2]).toMatchObject({
      data: { pages: [10] },
      hasNextPage: undefined,
      hasPreviousPage: true,
      isFetching: true,
      isFetchingNextPage: false,
      isFetchingPreviousPage: true,
      isSuccess: true,
    })
    expect(states[3]).toMatchObject({
      data: { pages: [9, 10] },
      hasNextPage: undefined,
      hasPreviousPage: true,
      isFetching: false,
      isFetchingNextPage: false,
      isFetchingPreviousPage: false,
      isSuccess: true,
    })
  })

  it('should be able to refetch when providing page params manually', async () => {
    const key = queryKey()
    const states: UseInfiniteQueryResult<number>[] = []

    function Page() {
      const state = useInfiniteQuery(key, ({ pageParam = 10 }) =>
        Number(pageParam)
      )

      states.push(state)

      const { fetchNextPage, fetchPreviousPage, refetch } = state

      React.useEffect(() => {
        setActTimeout(() => {
          fetchNextPage({ pageParam: 11 })
        }, 10)
        setActTimeout(() => {
          fetchPreviousPage({ pageParam: 9 })
        }, 20)
        setActTimeout(() => {
          refetch()
        }, 30)
      }, [fetchNextPage, fetchPreviousPage, refetch])

      return null
    }

    renderWithClient(queryClient, <Page />)

    await sleep(100)

    expect(states.length).toBe(8)
    // Initial fetch
    expect(states[0]).toMatchObject({
      data: undefined,
      isFetching: true,
      isFetchingNextPage: false,
    })
    // Initial fetch done
    expect(states[1]).toMatchObject({
      data: { pages: [10] },
      isFetching: false,
      isFetchingNextPage: false,
    })
    // Fetch next page
    expect(states[2]).toMatchObject({
      data: { pages: [10] },
      isFetching: true,
      isFetchingNextPage: true,
    })
    // Fetch next page done
    expect(states[3]).toMatchObject({
      data: { pages: [10, 11] },
      isFetching: false,
      isFetchingNextPage: false,
    })
    // Fetch previous page
    expect(states[4]).toMatchObject({
      data: { pages: [10, 11] },
      isFetching: true,
      isFetchingNextPage: false,
      isFetchingPreviousPage: true,
    })
    // Fetch previous page done
    expect(states[5]).toMatchObject({
      data: { pages: [9, 10, 11] },
      isFetching: false,
      isFetchingNextPage: false,
      isFetchingPreviousPage: false,
    })
    // Refetch
    expect(states[6]).toMatchObject({
      data: { pages: [9, 10, 11] },
      isFetching: true,
      isFetchingNextPage: false,
      isFetchingPreviousPage: false,
    })
    // Refetch done
    expect(states[7]).toMatchObject({
      data: { pages: [9, 10, 11] },
      isFetching: false,
      isFetchingNextPage: false,
      isFetchingPreviousPage: false,
    })
  })

  it('should be able to refetch when providing page params automatically', async () => {
    const key = queryKey()
    const states: UseInfiniteQueryResult<number>[] = []

    function Page() {
      const state = useInfiniteQuery(
        key,
        ({ pageParam = 10 }) => Number(pageParam),
        {
          getPreviousPageParam: firstPage => firstPage - 1,
          getNextPageParam: lastPage => lastPage + 1,
        }
      )

      states.push(state)

      const { fetchNextPage, fetchPreviousPage, refetch } = state

      React.useEffect(() => {
        setActTimeout(() => {
          fetchNextPage()
        }, 10)
        setActTimeout(() => {
          fetchPreviousPage()
        }, 20)
        setActTimeout(() => {
          refetch()
        }, 30)
      }, [fetchNextPage, fetchPreviousPage, refetch])

      return null
    }

    renderWithClient(queryClient, <Page />)

    await sleep(100)

    expect(states.length).toBe(8)
    // Initial fetch
    expect(states[0]).toMatchObject({
      data: undefined,
      isFetching: true,
      isFetchingNextPage: false,
    })
    // Initial fetch done
    expect(states[1]).toMatchObject({
      data: { pages: [10] },
      isFetching: false,
      isFetchingNextPage: false,
    })
    // Fetch next page
    expect(states[2]).toMatchObject({
      data: { pages: [10] },
      isFetching: true,
      isFetchingNextPage: true,
    })
    // Fetch next page done
    expect(states[3]).toMatchObject({
      data: { pages: [10, 11] },
      isFetching: false,
      isFetchingNextPage: false,
    })
    // Fetch previous page
    expect(states[4]).toMatchObject({
      data: { pages: [10, 11] },
      isFetching: true,
      isFetchingNextPage: false,
      isFetchingPreviousPage: true,
    })
    // Fetch previous page done
    expect(states[5]).toMatchObject({
      data: { pages: [9, 10, 11] },
      isFetching: false,
      isFetchingNextPage: false,
      isFetchingPreviousPage: false,
    })
    // Refetch
    expect(states[6]).toMatchObject({
      data: { pages: [9, 10, 11] },
      isFetching: true,
      isFetchingNextPage: false,
      isFetchingPreviousPage: false,
    })
    // Refetch done
    expect(states[7]).toMatchObject({
      data: { pages: [9, 10, 11] },
      isFetching: false,
      isFetchingNextPage: false,
      isFetchingPreviousPage: false,
    })
  })

  it('should silently cancel any ongoing fetch when fetching more', async () => {
    const key = queryKey()
    const states: UseInfiniteQueryResult<number>[] = []

    function Page() {
      const start = 10
      const state = useInfiniteQuery(
        key,
        async ({ pageParam = start }) => {
          await sleep(50)
          return Number(pageParam)
        },
        {
          getNextPageParam: lastPage => lastPage + 1,
        }
      )

      states.push(state)

      const { refetch, fetchNextPage } = state

      React.useEffect(() => {
        setActTimeout(() => {
          refetch()
        }, 100)
        setActTimeout(() => {
          fetchNextPage()
        }, 110)
      }, [fetchNextPage, refetch])

      return null
    }

    renderWithClient(queryClient, <Page />)

    await sleep(300)

    expect(states.length).toBe(5)
    expect(states[0]).toMatchObject({
      hasNextPage: undefined,
      data: undefined,
      isFetching: true,
      isFetchingNextPage: false,
      isSuccess: false,
    })
    expect(states[1]).toMatchObject({
      hasNextPage: true,
      data: { pages: [10] },
      isFetching: false,
      isFetchingNextPage: false,
      isSuccess: true,
    })
    expect(states[2]).toMatchObject({
      hasNextPage: true,
      data: { pages: [10] },
      isFetching: true,
      isFetchingNextPage: false,
      isSuccess: true,
    })
    expect(states[3]).toMatchObject({
      hasNextPage: true,
      data: { pages: [10] },
      isFetching: true,
      isFetchingNextPage: true,
      isSuccess: true,
    })
    expect(states[4]).toMatchObject({
      hasNextPage: true,
      data: { pages: [10, 11] },
      isFetching: false,
      isFetchingNextPage: false,
      isSuccess: true,
    })
  })

  it('should keep fetching first page when not loaded yet and triggering fetch more', async () => {
    const key = queryKey()
    const states: UseInfiniteQueryResult<number>[] = []

    function Page() {
      const start = 10
      const state = useInfiniteQuery(
        key,
        async ({ pageParam = start }) => {
          await sleep(50)
          return Number(pageParam)
        },
        {
          getNextPageParam: lastPage => lastPage + 1,
        }
      )

      states.push(state)

      const { fetchNextPage } = state

      React.useEffect(() => {
        setActTimeout(() => {
          fetchNextPage()
        }, 10)
      }, [fetchNextPage])

      return null
    }

    renderWithClient(queryClient, <Page />)

    await sleep(100)

    expect(states.length).toBe(2)
    expect(states[0]).toMatchObject({
      hasNextPage: undefined,
      data: undefined,
      isFetching: true,
      isFetchingNextPage: false,
      isSuccess: false,
    })
    expect(states[1]).toMatchObject({
      hasNextPage: true,
      data: { pages: [10] },
      isFetching: false,
      isFetchingNextPage: false,
      isSuccess: true,
    })
  })

  it('should be able to override the cursor in the fetchNextPage callback', async () => {
    const key = queryKey()
    const states: UseInfiniteQueryResult<number>[] = []

    function Page() {
      const state = useInfiniteQuery(
        key,
        async ({ pageParam = 0 }) => {
          await sleep(10)
          return Number(pageParam)
        },
        {
          getNextPageParam: lastPage => lastPage + 1,
        }
      )

      states.push(state)

      const { fetchNextPage } = state

      React.useEffect(() => {
        setActTimeout(() => {
          fetchNextPage({ pageParam: 5 })
        }, 20)
      }, [fetchNextPage])

      return null
    }

    renderWithClient(queryClient, <Page />)

    await sleep(100)

    expect(states.length).toBe(4)
    expect(states[0]).toMatchObject({
      hasNextPage: undefined,
      data: undefined,
      isFetching: true,
      isFetchingNextPage: false,
      isSuccess: false,
    })
    expect(states[1]).toMatchObject({
      hasNextPage: true,
      data: { pages: [0] },
      isFetching: false,
      isFetchingNextPage: false,
      isSuccess: true,
    })
    expect(states[2]).toMatchObject({
      hasNextPage: true,
      data: { pages: [0] },
      isFetching: true,
      isFetchingNextPage: true,
      isSuccess: true,
    })
    expect(states[3]).toMatchObject({
      hasNextPage: true,
      data: { pages: [0, 5] },
      isFetching: false,
      isFetchingNextPage: false,
      isSuccess: true,
    })
  })

  it('should be able to set new pages with the query client', async () => {
    const key = queryKey()
    const states: UseInfiniteQueryResult<number>[] = []

    function Page() {
      const [firstPage, setFirstPage] = React.useState(0)

      const state = useInfiniteQuery(
        key,
        async ({ pageParam = firstPage }) => {
          await sleep(10)
          return Number(pageParam)
        },
        {
          getNextPageParam: lastPage => lastPage + 1,
        }
      )

      states.push(state)

      const { refetch } = state

      React.useEffect(() => {
        setActTimeout(() => {
          queryClient.setQueryData(key, { pages: [7, 8], pageParams: [7, 8] })
          setFirstPage(7)
        }, 20)

        setActTimeout(() => {
          refetch()
        }, 50)
      }, [refetch])

      return null
    }

    renderWithClient(queryClient, <Page />)

    await sleep(100)

    expect(states.length).toBe(6)
    expect(states[0]).toMatchObject({
      hasNextPage: undefined,
      data: undefined,
      isFetching: true,
      isFetchingNextPage: false,
      isSuccess: false,
    })
    // After first fetch
    expect(states[1]).toMatchObject({
      hasNextPage: true,
      data: { pages: [0] },
      isFetching: false,
      isFetchingNextPage: false,
      isSuccess: true,
    })
    // Set state
    expect(states[2]).toMatchObject({
      hasNextPage: true,
      data: { pages: [0] },
      isFetching: false,
      isFetchingNextPage: false,
      isSuccess: true,
    })
    // Cache update
    expect(states[3]).toMatchObject({
      hasNextPage: true,
      data: { pages: [7, 8] },
      isFetching: false,
      isFetchingNextPage: false,
      isSuccess: true,
    })
    // Refetch
    expect(states[4]).toMatchObject({
      hasNextPage: true,
      data: { pages: [7, 8] },
      isFetching: true,
      isFetchingNextPage: false,
      isSuccess: true,
    })
    // Refetch done
    expect(states[5]).toMatchObject({
      hasNextPage: true,
      data: { pages: [7, 8] },
      isFetching: false,
      isFetchingNextPage: false,
      isSuccess: true,
    })
  })

  it('should set hasNextPage to false if getNextPageParam returns undefined', async () => {
    const key = queryKey()
    const states: UseInfiniteQueryResult<number>[] = []

    function Page() {
      const state = useInfiniteQuery(
        key,
        ({ pageParam = 1 }) => Number(pageParam),
        {
          getNextPageParam: () => undefined,
        }
      )

      states.push(state)

      return null
    }

    renderWithClient(queryClient, <Page />)

    await sleep(100)

    expect(states.length).toBe(2)
    expect(states[0]).toMatchObject({
      data: undefined,
      hasNextPage: undefined,
      isFetching: true,
      isFetchingNextPage: false,
      isSuccess: false,
    })
    expect(states[1]).toMatchObject({
      data: { pages: [1] },
      hasNextPage: false,
      isFetching: false,
      isFetchingNextPage: false,
      isSuccess: true,
    })
  })

  it('should compute hasNextPage correctly using initialData', async () => {
    const key = queryKey()
    const states: UseInfiniteQueryResult<number>[] = []

    function Page() {
      const state = useInfiniteQuery(key, ({ pageParam = 10 }) => pageParam, {
        initialData: { pages: [10], pageParams: [undefined] },
        getNextPageParam: lastPage => (lastPage === 10 ? 11 : undefined),
      })

      states.push(state)

      return null
    }

    renderWithClient(queryClient, <Page />)

    await sleep(100)

    expect(states.length).toBe(2)
    expect(states[0]).toMatchObject({
      data: { pages: [10] },
      hasNextPage: true,
      isFetching: true,
      isFetchingNextPage: false,
      isSuccess: true,
    })
    expect(states[1]).toMatchObject({
      data: { pages: [10] },
      hasNextPage: true,
      isFetching: false,
      isFetchingNextPage: false,
      isSuccess: true,
    })
  })

  it('should compute hasNextPage correctly for falsy getFetchMore return value using initialData', async () => {
    const key = queryKey()
    const states: UseInfiniteQueryResult<number>[] = []

    function Page() {
      const state = useInfiniteQuery(key, ({ pageParam = 10 }) => pageParam, {
        initialData: { pages: [10], pageParams: [undefined] },
        getNextPageParam: () => undefined,
      })

      states.push(state)

      return null
    }

    renderWithClient(queryClient, <Page />)

    await sleep(100)

    expect(states.length).toBe(2)
    expect(states[0]).toMatchObject({
      data: { pages: [10] },
      hasNextPage: false,
      isFetching: true,
      isFetchingNextPage: false,
      isSuccess: true,
    })
    expect(states[1]).toMatchObject({
      data: { pages: [10] },
      hasNextPage: false,
      isFetching: false,
      isFetchingNextPage: false,
      isSuccess: true,
    })
  })

  it('should build fresh cursors on refetch', async () => {
    const key = queryKey()

    const genItems = (size: number) =>
      [...new Array(size)].fill(null).map((_, d) => d)
    const items = genItems(15)
    const limit = 3

    const fetchItemsWithLimit = async (cursor = 0, ts: number) => {
      await sleep(10)
      return {
        nextId: cursor + limit,
        items: items.slice(cursor, cursor + limit),
        ts,
      }
    }

    function Page() {
      const fetchCountRef = React.useRef(0)
      const {
        status,
        data,
        error,
        isFetchingNextPage,
        fetchNextPage,
        hasNextPage,
        refetch,
      } = useInfiniteQuery<Result, Error>(
        key,
        ({ pageParam = 0 }) =>
          fetchItemsWithLimit(pageParam, fetchCountRef.current++),
        {
          getNextPageParam: lastPage => lastPage.nextId,
        }
      )

      return (
        <div>
          <h1>Pagination</h1>
          {status === 'loading' ? (
            'Loading...'
          ) : status === 'error' ? (
            <span>Error: {error?.message}</span>
          ) : (
            <>
              <div>Data:</div>
              {data?.pages.map((page, i) => (
                <div key={i}>
                  <div>
                    Page {i}: {page.ts}
                  </div>
                  <div key={i}>
                    {page.items.map(item => (
                      <p key={item}>Item: {item}</p>
                    ))}
                  </div>
                </div>
              ))}
              <div>
                <button
                  onClick={() => fetchNextPage()}
                  disabled={!hasNextPage || Boolean(isFetchingNextPage)}
                >
                  {isFetchingNextPage
                    ? 'Loading more...'
                    : hasNextPage
                    ? 'Load More'
                    : 'Nothing more to load'}
                </button>
                <button onClick={() => refetch()}>Refetch</button>
                <button
                  onClick={() => {
                    // Imagine that this mutation happens somewhere else
                    // makes an actual network request
                    // and calls invalidateQueries in an onSuccess
                    items.splice(4, 1)
                    queryClient.invalidateQueries(key)
                  }}
                >
                  Remove item
                </button>
              </div>
              <div>{!isFetchingNextPage ? 'Background Updating...' : null}</div>
            </>
          )}
        </div>
      )
    }

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

    rendered.getByText('Loading...')

    await waitFor(() => rendered.getByText('Item: 2'))
    await waitFor(() => rendered.getByText('Page 0: 0'))

    fireEvent.click(rendered.getByText('Load More'))

    await waitFor(() => rendered.getByText('Loading more...'))
    await waitFor(() => rendered.getByText('Item: 5'))
    await waitFor(() => rendered.getByText('Page 0: 0'))
    await waitFor(() => rendered.getByText('Page 1: 1'))

    fireEvent.click(rendered.getByText('Load More'))

    await waitFor(() => rendered.getByText('Loading more...'))
    await waitFor(() => rendered.getByText('Item: 8'))
    await waitFor(() => rendered.getByText('Page 0: 0'))
    await waitFor(() => rendered.getByText('Page 1: 1'))
    await waitFor(() => rendered.getByText('Page 2: 2'))

    fireEvent.click(rendered.getByText('Refetch'))

    await waitFor(() => rendered.getByText('Background Updating...'))
    await waitFor(() => rendered.getByText('Item: 8'))
    await waitFor(() => rendered.getByText('Page 0: 3'))
    await waitFor(() => rendered.getByText('Page 1: 4'))
    await waitFor(() => rendered.getByText('Page 2: 5'))

    // ensure that Item: 4 is rendered before removing it
    expect(rendered.queryAllByText('Item: 4')).toHaveLength(1)

    // remove Item: 4
    fireEvent.click(rendered.getByText('Remove item'))

    await waitFor(() => rendered.getByText('Background Updating...'))
    // ensure that an additional item is rendered (it means that cursors were properly rebuilt)
    await waitFor(() => rendered.getByText('Item: 9'))
    await waitFor(() => rendered.getByText('Page 0: 6'))
    await waitFor(() => rendered.getByText('Page 1: 7'))
    await waitFor(() => rendered.getByText('Page 2: 8'))

    // ensure that Item: 4 is no longer rendered
    expect(rendered.queryAllByText('Item: 4')).toHaveLength(0)
  })

  it('should compute hasNextPage correctly for falsy getFetchMore return value on refetching', async () => {
    const key = queryKey()
    const MAX = 2

    function Page() {
      const fetchCountRef = React.useRef(0)
      const [isRemovedLastPage, setIsRemovedLastPage] = React.useState<boolean>(
        false
      )
      const {
        status,
        data,
        error,
        isFetching,
        isFetchingNextPage,
        fetchNextPage,
        hasNextPage,
        refetch,
      } = useInfiniteQuery<Result, Error>(
        key,
        ({ pageParam = 0 }) =>
          fetchItems(
            pageParam,
            fetchCountRef.current++,
            pageParam === MAX || (pageParam === MAX - 1 && isRemovedLastPage)
          ),
        {
          getNextPageParam: lastPage => lastPage.nextId,
        }
      )

      return (
        <div>
          <h1>Pagination</h1>
          {status === 'loading' ? (
            'Loading...'
          ) : status === 'error' ? (
            <span>Error: {error?.message}</span>
          ) : (
            <>
              <div>Data:</div>
              {data?.pages.map((page, i) => (
                <div key={i}>
                  <div>
                    Page {i}: {page.ts}
                  </div>
                  <div key={i}>
                    {page.items.map(item => (
                      <p key={item}>Item: {item}</p>
                    ))}
                  </div>
                </div>
              ))}
              <div>
                <button
                  onClick={() => fetchNextPage()}
                  disabled={!hasNextPage || Boolean(isFetchingNextPage)}
                >
                  {isFetchingNextPage
                    ? 'Loading more...'
                    : hasNextPage
                    ? 'Load More'
                    : 'Nothing more to load'}
                </button>
                <button onClick={() => refetch()}>Refetch</button>
                <button onClick={() => setIsRemovedLastPage(true)}>
                  Remove Last Page
                </button>
              </div>
              <div>
                {isFetching && !isFetchingNextPage
                  ? 'Background Updating...'
                  : null}
              </div>
            </>
          )}
        </div>
      )
    }

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

    rendered.getByText('Loading...')

    await waitFor(() => {
      rendered.getByText('Item: 9')
      rendered.getByText('Page 0: 0')
    })

    fireEvent.click(rendered.getByText('Load More'))

    await waitFor(() => rendered.getByText('Loading more...'))

    await waitFor(() => {
      rendered.getByText('Item: 19')
      rendered.getByText('Page 0: 0')
      rendered.getByText('Page 1: 1')
    })

    fireEvent.click(rendered.getByText('Load More'))

    await waitFor(() => rendered.getByText('Loading more...'))

    await waitFor(() => {
      rendered.getByText('Item: 29')
      rendered.getByText('Page 0: 0')
      rendered.getByText('Page 1: 1')
      rendered.getByText('Page 2: 2')
    })

    rendered.getByText('Nothing more to load')

    fireEvent.click(rendered.getByText('Remove Last Page'))

    await sleep(10)

    fireEvent.click(rendered.getByText('Refetch'))

    await waitFor(() => rendered.getByText('Background Updating...'))

    await waitFor(() => {
      rendered.getByText('Page 0: 3')
      rendered.getByText('Page 1: 4')
    })

    expect(rendered.queryByText('Item: 29')).toBeNull()
    expect(rendered.queryByText('Page 2: 5')).toBeNull()

    rendered.getByText('Nothing more to load')
  })

  it('should cancel the query function when there are no more subscriptions', async () => {
    const key = queryKey()
    let cancelFn: jest.Mock = jest.fn()

    const queryFn = () => {
      const promise = new Promise<string>((resolve, reject) => {
        cancelFn = jest.fn(() => reject('Cancelled'))
        sleep(10).then(() => resolve('OK'))
      })

      ;(promise as any).cancel = cancelFn

      return promise
    }

    function Page() {
      const state = useInfiniteQuery(key, queryFn)
      return (
        <div>
          <h1>Status: {state.status}</h1>
        </div>
      )
    }

    const rendered = renderWithClient(
      queryClient,
      <Blink duration={5}>
        <Page />
      </Blink>
    )

    await waitFor(() => rendered.getByText('off'))

    expect(cancelFn).toHaveBeenCalled()
  })
})


import type {
  FetchNextPageOptions,
  FetchPreviousPageOptions,
  InfiniteData,
  InfiniteQueryObserverOptions,
  InfiniteQueryObserverResult,
} from './types'
import type { QueryClient } from './queryClient'
import { ObserverFetchOptions, QueryObserver } from './queryObserver'
import {
  hasNextPage,
  hasPreviousPage,
  infiniteQueryBehavior,
} from './infiniteQueryBehavior'

type InfiniteQueryObserverListener<TData, TError> = (
  result: InfiniteQueryObserverResult<TData, TError>
) => void

export class InfiniteQueryObserver<
  TData = unknown,
  TError = unknown,
  TQueryFnData = TData,
  TQueryData = TQueryFnData
> extends QueryObserver<
  InfiniteData<TData>,
  TError,
  TQueryFnData,
  InfiniteData<TQueryData>
> {
  // Type override
  subscribe!: (
    listener?: InfiniteQueryObserverListener<TData, TError>
  ) => () => void

  // Type override
  getCurrentResult!: () => InfiniteQueryObserverResult<TData, TError>

  // Type override
  protected fetch!: (
    fetchOptions?: ObserverFetchOptions
  ) => Promise<InfiniteQueryObserverResult<TData, TError>>

  // eslint-disable-next-line @typescript-eslint/no-useless-constructor
  constructor(
    client: QueryClient,
    options: InfiniteQueryObserverOptions<
      TData,
      TError,
      TQueryFnData,
      TQueryData
    >
  ) {
    super(client, options)
  }

  protected bindMethods(): void {
    super.bindMethods()
    this.fetchNextPage = this.fetchNextPage.bind(this)
    this.fetchPreviousPage = this.fetchPreviousPage.bind(this)
  }

  setOptions(
    options?: InfiniteQueryObserverOptions<
      TData,
      TError,
      TQueryFnData,
      TQueryData
    >
  ): void {
    super.setOptions({
      ...options,
      behavior: infiniteQueryBehavior<TData, TError, TQueryFnData>(),
    })
  }

  fetchNextPage(
    options?: FetchNextPageOptions
  ): Promise<InfiniteQueryObserverResult<TData, TError>> {
    return this.fetch({
      cancelRefetch: true,
      throwOnError: options?.throwOnError,
      meta: {
        fetchMore: { direction: 'forward', pageParam: options?.pageParam },
      },
    })
  }

  fetchPreviousPage(
    options?: FetchPreviousPageOptions
  ): Promise<InfiniteQueryObserverResult<TData, TError>> {
    return this.fetch({
      cancelRefetch: true,
      throwOnError: options?.throwOnError,
      meta: {
        fetchMore: { direction: 'backward', pageParam: options?.pageParam },
      },
    })
  }

  protected getNewResult(
    willFetch?: boolean
  ): InfiniteQueryObserverResult<TData, TError> {
    const { state } = this.getCurrentQuery()
    const result = super.getNewResult(willFetch)
    return {
      ...result,
      fetchNextPage: this.fetchNextPage,
      fetchPreviousPage: this.fetchPreviousPage,
      hasNextPage: hasNextPage(this.options, result.data?.pages),
      hasPreviousPage: hasPreviousPage(this.options, result.data?.pages),
      isFetchingNextPage:
        state.isFetching && state.fetchMeta?.fetchMore?.direction === 'forward',
      isFetchingPreviousPage:
        state.isFetching &&
        state.fetchMeta?.fetchMore?.direction === 'backward',
    }
  }
}


import { useState, useCallback, useEffect } from 'react'

const useMediaQuery = width => {
  const [targetReached, setTargetReached] = useState(false)
  const updateTarget = useCallback(e => {
    if (e.matches) {
      setTargetReached(true)
    } else {
      setTargetReached(false)
    }
  }, [])
  useEffect(() => {
    const media = window.matchMedia(`(max-width: ${width}px)`)
    media.addListener(updateTarget) // Check on mount (callback is not called until a change occurs)

    if (media.matches) {
      setTargetReached(true)
    }

    return () => media.removeListener(updateTarget)
  }, [])
  return targetReached
}

const useIsMobile = () => {
  return useMediaQuery(1024)
}

export { useMediaQuery, useIsMobile }


---
id: query-cancellation
title: Query Cancellation
---

By default, queries that unmount or become unused before their promises are resolved are simply ignored instead of canceled. Why is this?

- For most applications, ignoring out-of-date queries is sufficient.
- Cancellation APIs may not be available for every query function.
- If cancellation APIs are available, they typically vary in implementation between utilities/libraries (eg. Fetch vs Axios vs XMLHttpRequest).

But don't worry! If your queries are high-bandwidth or potentially very expensive to download, React Query exposes a generic way to **cancel** query requests using a cancellation token or other related API. To integrate with this feature, attach a `cancel` function to the promise returned by your query that implements your request cancellation. When a query becomes out-of-date or inactive, this `promise.cancel` function will be called (if available):

## Using `axios`

```js
import { CancelToken } from 'axios'

const query = useQuery('todos', () => {
  // Create a new CancelToken source for this request
  const source = CancelToken.source()

  const promise = axios.get('/todos', {
    // Pass the source token to your request
    cancelToken: source.token,
  })

  // Cancel the request if React Query calls the `promise.cancel` method
  promise.cancel = () => {
    source.cancel('Query was cancelled by React Query')
  }

  return promise
})
```

## Using `fetch`

```js
const query = useQuery('todos', () => {
  // Create a new AbortController instance for this request
  const controller = new AbortController()
  // Get the abortController's signal
  const signal = controller.signal

  const promise = fetch('/todos', {
    method: 'get',
    // Pass the signal to your request
    signal,
  })

  // Cancel the request if React Query calls the `promise.cancel` method
  promise.cancel = () => controller.abort()

  return promise
})
```

## Manual Cancellation

You might want to cancel a query manually. For example, if the request takes a long time to finish, you can allow the user to click a cancel button to stop the request. To do this, you just need to call `cache.cancelQueries(key)`. If `promise.cancel` is available, React Query will cancel the request.

```js
const [queryKey] = useState('todos')

const query = useQuery(queryKey, () => {
  const controller = new AbortController()
  const signal = controller.signal

  const promise = fetch('/todos', {
    method: 'get',
    signal,
  })

  // Cancel the request if React Query calls the `promise.cancel` method
  promise.cancel = () => controller.abort()

  return promise
})

const cache = useQueryCache();

return (
  <button onClick={(e) => {
    e.preventDefault();
    cache.cancelQueries(queryKey);
   }}>Cancel</button>
)
```


isSuccess: true,
isPreviousData: false,
})
// Previous data
expect(states[3]).toMatchObject({


# Example

To run this example:

- `npm install` or `yarn`
- `npm run start` or `yarn start`


import React from 'react'
import axios from 'axios'
import {
  useQuery,
  useQueryClient,
  QueryClient,
  QueryClientProvider,
} from 'react-query'
import { ReactQueryDevtools } from 'react-query/devtools'

const getCharacters = async () => {
  await new Promise(r => setTimeout(r, 500))
  const { data } = await axios.get('https://rickandmortyapi.com/api/character/')
  return data
}

const getCharacter = async selectedChar => {
  await new Promise(r => setTimeout(r, 500))
  const { data } = await axios.get(
    `https://rickandmortyapi.com/api/character/${selectedChar}`
  )
  return data
}

const queryClient = new QueryClient()

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Example />
    </QueryClientProvider>
  )
}

function Example() {
  const queryClient = useQueryClient()
  const rerender = React.useState(0)[1]
  const [selectedChar, setSelectedChar] = React.useState(1)

  const charactersQuery = useQuery('characters', getCharacters)

  const characterQuery = useQuery(['character', selectedChar], () =>
    getCharacter(selectedChar)
  )

  return (

        Hovering over a character will prefetch it, and when it's been
        prefetched it will turn bold. Clicking on a prefetched
        character will show their stats below immediately.

Characters

      {charactersQuery.isLoading ? (
        'Loading...'
      ) : (
        <>

            {charactersQuery.data?.results.map(char => (

 {
                  setSelectedChar(char.id)
                }}
                onMouseEnter={async () => {
                  await queryClient.prefetchQuery(
                    ['character', char.id],
                    () => getCharacter(char.id),
                    {
                      staleTime: 10 * 1000, // only prefetch if older than 10 seconds
                    }
                  )

                  setTimeout(() => {
                    rerender({})
                  }, 1)
                }}
              >

                  {char.id} - {char.name}

            ))}

Selected Character

          {characterQuery.isLoading ? (
            'Loading...'
          ) : (
            <>

{JSON.stringify(characterQuery.data, null, 2)}

            </>
          )}
          <ReactQueryDevtools initialIsOpen />
        </>
      )}

  )
}


import React from "react";
import ReactDOM from "react-dom";

import {
  QueryClient,
  QueryClientProvider,
  useQuery,
  useQueryClient,
  useMutation,
} from "react-query";

import { ReactQueryDevtools } from "react-query/devtools";

import "./styles.css";

let id = 0;
let list = [
  "apple",
  "banana",
  "pineapple",
  "grapefruit",
  "dragonfruit",
  "grapes",
].map((d) => ({ id: id++, name: d, notes: "These are some notes" }));

let errorRate = 0.05;
let queryTimeMin = 1000;
let queryTimeMax = 2000;

const queryClient = new QueryClient();

function Root() {
  const [staleTime, setStaleTime] = React.useState(1000);
  const [cacheTime, setCacheTime] = React.useState(3000);
  const [localErrorRate, setErrorRate] = React.useState(errorRate);
  const [localFetchTimeMin, setLocalFetchTimeMin] = React.useState(
    queryTimeMin
  );
  const [localFetchTimeMax, setLocalFetchTimeMax] = React.useState(
    queryTimeMax
  );

  React.useEffect(() => {
    errorRate = localErrorRate;
    queryTimeMin = localFetchTimeMin;
    queryTimeMax = localFetchTimeMax;
  }, [localErrorRate, localFetchTimeMax, localFetchTimeMin]);

  React.useEffect(() => {
    queryClient.setDefaultOptions({
      queries: {
        staleTime,
        cacheTime,
      },
    });
  }, [cacheTime, staleTime]);

  return (
    <QueryClientProvider client={queryClient}>

        The "staleTime" and "cacheTime" durations have been altered in this
        example to show how query stale-ness and query caching work on a
        granular level

        Stale Time:{" "}
        <input
          type="number"
          min="0"
          step="1000"
          value={staleTime}
          onChange={(e) => setStaleTime(parseFloat(e.target.value, 10))}
          style={{ width: "100px" }}
        />

        Cache Time:{" "}
        <input
          type="number"
          min="0"
          step="1000"
          value={cacheTime}
          onChange={(e) => setCacheTime(parseFloat(e.target.value, 10))}
          style={{ width: "100px" }}
        />

        Error Rate:{" "}
        <input
          type="number"
          min="0"
          max="1"
          step=".05"
          value={localErrorRate}
          onChange={(e) => setErrorRate(parseFloat(e.target.value, 10))}
          style={{ width: "100px" }}
        />

        Fetch Time Min:{" "}
        <input
          type="number"
          min="1"
          step="500"
          value={localFetchTimeMin}
          onChange={(e) => setLocalFetchTimeMin(parseFloat(e.target.value, 10))}
          style={{ width: "60px" }}
        />{" "}

        Fetch Time Max:{" "}
        <input
          type="number"
          min="1"
          step="500"
          value={localFetchTimeMax}
          onChange={(e) => setLocalFetchTimeMax(parseFloat(e.target.value, 10))}
          style={{ width: "60px" }}
        />

      <App />

      <ReactQueryDevtools initialIsOpen />
    </QueryClientProvider>
  );
}

function App() {
  const queryClient = useQueryClient();
  const [editingIndex, setEditingIndex] = React.useState(null);
  const [views, setViews] = React.useState(["", "fruit", "grape"]);
  // const [views, setViews] = React.useState([""]);

  return (

        <button onClick={() => queryClient.invalidateQueries(true)}>
          Force Refetch All
        </button>

      {views.map((view, index) => (

          <Todos
            initialFilter={view}
            setEditingIndex={setEditingIndex}
            onRemove={() => {
              setViews((old) => [...old, ""]);
            }}
          />

      ))}
      <button
        onClick={() => {
          setViews((old) => [...old, ""]);
        }}
      >
        Add Filter List
      </button>

      {editingIndex !== null ? (
        <>
          <EditTodo
            editingIndex={editingIndex}
            setEditingIndex={setEditingIndex}
          />

        </>
      ) : null}
      <AddTodo />

  );
}

function Todos({ initialFilter = "", setEditingIndex }) {
  const [filter, setFilter] = React.useState(initialFilter);

  const { status, data, isFetching, error, failureCount, refetch } = useQuery(
    ["todos", { filter }],
    () => fetchTodos({ filter })
  );

  return (

          Filter:{" "}
          <input value={filter} onChange={(e) => setFilter(e.target.value)} />

      {status === "loading" ? (
        Loading... (Attempt: {failureCount + 1})
      ) : status === "error" ? (

          Error: {error.message}

          <button onClick={() => refetch()}>Retry</button>

      ) : (
        <>

            {data
              ? data.map((todo) => (

                    {todo.name}{" "}
                    <button onClick={() => setEditingIndex(todo.id)}>
                      Edit
                    </button>

                ))
              : null}

            {isFetching ? (

                Background Refreshing... (Attempt: {failureCount + 1})

            ) : (

            )}

        </>
      )}

  );
}

function EditTodo({ editingIndex, setEditingIndex }) {
  const queryClient = useQueryClient();

  // Don't attempt to query until editingIndex is truthy
  const { status, data, isFetching, error, failureCount, refetch } = useQuery(
    ["todo", { id: editingIndex }],
    () => fetchTodoById({ id: editingIndex }),
    {
      enabled: editingIndex !== null,
    }
  );

  const [todo, setTodo] = React.useState(data || {});

  React.useEffect(() => {
    if (editingIndex !== null && data) {
      setTodo(data);
    } else {
      setTodo({});
    }
  }, [data, editingIndex]);

  const saveMutation = useMutation(patchTodo, {
    onSuccess: (data) => {
      // Update `todos` and the individual todo queries when this mutation succeeds
      queryClient.invalidateQueries("todos");
      queryClient.setQueryData(["todo", { id: editingIndex }], data);
    },
  });

  const onSave = () => {
    saveMutation.mutate(todo);
  };

  const disableEditSave =
    status === "loading" || saveMutation.status === "loading";

  return (

        {data ? (
          <>
            <button onClick={() => setEditingIndex(null)}>Back</button> Editing
            Todo "{data.name}" (#
            {editingIndex})
          </>
        ) : null}

      {status === "loading" ? (
        Loading... (Attempt: {failureCount + 1})
      ) : error ? (

          Error! <button onClick={() => refetch()}>Retry</button>

      ) : (
        <>

            Name:{" "}
            <input
              value={todo.name}
              onChange={(e) =>
                e.persist() ||
                setTodo((old) => ({ ...old, name: e.target.value }))
              }
              disabled={disableEditSave}
            />

            Notes:{" "}
            <input
              value={todo.notes}
              onChange={(e) =>
                e.persist() ||
                setTodo((old) => ({ ...old, notes: e.target.value }))
              }
              disabled={disableEditSave}
            />

            <button onClick={onSave} disabled={disableEditSave}>
              Save
            </button>

            {saveMutation.status === "loading"
              ? "Saving..."
              : saveMutation.status === "error"
              ? saveMutation.error.message
              : "Saved!"}

            {isFetching ? (

                Background Refreshing... (Attempt: {failureCount + 1})

            ) : (

            )}

        </>
      )}

  );
}

function AddTodo() {
  const queryClient = useQueryClient();
  const [name, setName] = React.useState("");

  const addMutation = useMutation(postTodo, {
    onSuccess: () => {
      queryClient.invalidateQueries("todos");
    },
  });

  return (

      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        disabled={addMutation.status === "loading"}
      />
      <button
        onClick={() => {
          addMutation.mutate({ name });
        }}
        disabled={addMutation.status === "loading" || !name}
      >
        Add Todo
      </button>

        {addMutation.status === "loading"
          ? "Saving..."
          : addMutation.status === "error"
          ? addMutation.error.message
          : "Saved!"}

  );
}

function fetchTodos({ filter } = {}) {
  console.info("fetchTodos", { filter });
  const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random() < errorRate) {
        return reject(
          new Error(JSON.stringify({ fetchTodos: { filter } }, null, 2))
        );
      }
      resolve(list.filter((d) => d.name.includes(filter)));
    }, queryTimeMin + Math.random() * (queryTimeMax - queryTimeMin));
  });

  promise.cancel = () => console.info("cancelled", filter);

  return promise;
}

function fetchTodoById({ id }) {
  console.info("fetchTodoById", { id });
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random() < errorRate) {
        return reject(
          new Error(JSON.stringify({ fetchTodoById: { id } }, null, 2))
        );
      }
      resolve(list.find((d) => d.id == id));
    }, queryTimeMin + Math.random() * (queryTimeMax - queryTimeMin));
  });
}

function postTodo({ name, notes }) {
  console.info("postTodo", { name, notes });
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random() < errorRate) {
        return reject(
          new Error(JSON.stringify({ postTodo: { name, notes } }, null, 2))
        );
      }
      const todo = { name, notes, id: id++ };
      list = [...list, todo];
      resolve(todo);
    }, queryTimeMin + Math.random() * (queryTimeMax - queryTimeMin));
  });
}

function patchTodo(todo) {
  console.info("patchTodo", todo);
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random() < errorRate) {
        return reject(new Error(JSON.stringify({ patchTodo: todo }, null, 2)));
      }
      list = list.map((d) => {
        if (d.id === todo.id) {
          return todo;
        }
        return d;
      });
      resolve(todo);
    }, queryTimeMin + Math.random() * (queryTimeMax - queryTimeMin));
  });
}

const rootElement = document.getElementById("root");
ReactDOM.render(<Root />, rootElement);


import React from 'react'

import { notifyManager } from '../core/notifyManager'
import { QueryObserver } from '../core/queryObserver'
import { useQueryErrorResetBoundary } from './QueryErrorResetBoundary'
import { useQueryClient } from './QueryClientProvider'
import { UseBaseQueryOptions } from './types'

export function useBaseQuery<TData, TError, TQueryFnData, TQueryData>(
  options: UseBaseQueryOptions<TData, TError, TQueryFnData, TQueryData>,
  Observer: typeof QueryObserver
) {
  const queryClient = useQueryClient()
  const errorResetBoundary = useQueryErrorResetBoundary()
  const defaultedOptions = queryClient.defaultQueryObserverOptions(options)

  // Include callbacks in batch renders
  if (defaultedOptions.onError) {
    defaultedOptions.onError = notifyManager.batchCalls(
      defaultedOptions.onError
    )
  }

  if (defaultedOptions.onSuccess) {
    defaultedOptions.onSuccess = notifyManager.batchCalls(
      defaultedOptions.onSuccess
    )
  }

  if (defaultedOptions.onSettled) {
    defaultedOptions.onSettled = notifyManager.batchCalls(
      defaultedOptions.onSettled
    )
  }

  // Always set stale time when using suspense to prevent
  // fetching again when directly re-mounting after suspense
  if (
    defaultedOptions.suspense &&
    typeof defaultedOptions.staleTime !== 'number'
  ) {
    defaultedOptions.staleTime = 1000
  }

  // Create query observer
  const observerRef = React.useRef<QueryObserver<any, any, any, any>>()
  const observer =
    observerRef.current || new Observer(queryClient, defaultedOptions)
  observerRef.current = observer

  // Update options
  if (observer.hasListeners()) {
    observer.setOptions(defaultedOptions)
  }

  const [currentResult, setCurrentResult] = React.useState(() =>
    observer.getCurrentResult()
  )

  // Subscribe to the observer
  React.useEffect(() => {
    errorResetBoundary.clearReset()
    return observer.subscribe(notifyManager.batchCalls(setCurrentResult))
  }, [observer, errorResetBoundary])

  // Handle suspense
  if (observer.options.suspense || observer.options.useErrorBoundary) {
    if (
      currentResult.isError &&
      !errorResetBoundary.isReset() &&
      !observer.getCurrentQuery().isFetching()
    ) {
      throw currentResult.error
    }

    if (observer.options.suspense && currentResult.isLoading) {
      errorResetBoundary.clearReset()
      const unsubscribe = observer.subscribe()
      throw observer.refetch().finally(unsubscribe)
    }
  }

  return currentResult
}


# 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*


---
id: paginated-queries
title: Paginated / Lagged Queries
---

Rendering paginated data is a very common UI pattern and in React Query, it "just works" by including the page information in the query key:

```js
const result = useQuery(['projects', page], fetchProjects)
```

However, if you run this simple example, you might notice something strange:

**The UI jumps in and out of the `success` and `loading` states because each new page is treated like a brand new query.**

This experience is not optimal and unfortunately is how many tools today insist on working. But not React Query! As you may have guessed, React Query comes with an awesome featured called `keepPreviousData` that allows us to get around this.

## Better Paginated Queries with `keepPreviousData`

Consider the following example where we would ideally want to increment a pageIndex (or cursor) for a query. If we were to use `useQuery`, **it would still technically work fine**, but the UI would jump in and out of the `success` and `loading` states as different queries are created and destroyed for each page or cursor. By setting `keepPreviousData` to `true` we get a few new things:

- **The data from the last successful fetch available while new data is being requested, even though the query key has changed**.
- When the new data arrives, the previous `data` is seamlessly swapped to show the new data.
- `isPreviousData` is made available to know what data the query is currently providing you

```js
function Todos() {
  const [page, setPage] = React.useState(0)

  const fetchProjects = (page = 0) => fetch('/api/projects?page=' + page)

  const {
    isLoading,
    isError,
    error,
    data,
    isFetching,
    isPreviousData,
  } = useQuery(['projects', page], () => fetchProjects(page), { keepPreviousData : true })

  return (
    <div>
      {isLoading ? (
        <div>Loading...</div>
      ) : isError ? (
        <div>Error: {error.message}</div>
      ) : (
        <div>
          {data.projects.map(project => (
            <p key={project.id}>{project.name}</p>
          ))}
        </div>
      )}
      <span>Current Page: {page + 1}</span>
      <button
        onClick={() => setPage(old => Math.max(old - 1, 0))}
        disabled={page === 0}
      >
        Previous Page
      </button>{' '}
      <button
        onClick={() => {
          if (!isPreviousData && data.hasMore) {
            setPage(old => old + 1)
          }
        }}
        // Disable the Next Page button until we know a next page is available
        disabled={isPreviousData || !data.hasMore}
      >
        Next Page
      </button>
      {isFetching ? <span> Loading...</span> : null}{' '}
    </div>
  )
}
```

## Lagging Infinite Query results with `keepPreviousData`

While not as common, the `keepPreviousData` option also works flawlessly with the `useInfiniteQuery` hook, so you can seamlessly allow your users to continue to see cached data while infinite query keys change over time.


---
id: useQueries
title: useQueries
---

The `useQueries` hook can be used to fetch a variable number of queries:

```js
const results = useQueries([
  { queryKey: ['post', 1], queryFn: fetchPost },
  { queryKey: ['post', 2], queryFn: fetchPost },
])
```

**Options**

The `useQueries` hook accepts an array with query option objects identical to the [`useQuery` hook](#usequery).

**Returns**

The `useQueries` hook returns an array with all the query results.


expect(states[3]).toMatchObject({
data: 0,
isFetching: false,
isSuccess: true,
isPreviousData: false,
})
// Switched query key
expect(states[4]).toMatchObject({
data: 0,

keep keep keep keep replace keep keep keep keep


![React Query Header](https://github.com/tannerlinsley/react-query/raw/master/media/repo-dark.png)

Hooks for fetching, caching and updating asynchronous data in React

Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [React Table](https://github.com/tannerlinsley/react-table), [React Form](https://github.com/tannerlinsley/react-form), [React Charts](https://github.com/tannerlinsley/react-charts)

## Visit [react-query.tanstack.com](https://react-query.tanstack.com) for docs, guides, API and more!

## Quick Features

- Transport/protocol/backend agnostic data fetching (REST, GraphQL, promises, whatever!)
- Auto Caching + Refetching (stale-while-revalidate, Window Refocus, Polling/Realtime)
- Parallel + Dependent Queries
- Mutations + Reactive Query Refetching
- Multi-layer Cache + Automatic Garbage Collection
- Paginated + Cursor-based Queries
- Load-More + Infinite Scroll Queries w/ Scroll Recovery
- Request Cancellation
- [React Suspense](https://reactjs.org/docs/concurrent-mode-suspense.html) + Fetch-As-You-Render Query Prefetching
- Dedicated Devtools
-

   (depending on features imported)

### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)

# Contributors ✨

Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):

Tanner Linsley
💻 🤔 💡 🚧 👀

Andrew Cherniavskii
💻 🐛

Thibaut Tiberghien
📖

Rohit Garg
🔧

Avinash
💻 🐛

Jason O'Neill
🚧 ⚠

Jack Zhao
💻 🐛

dpyzo0o
💻

Jelte Liekens
💻

Jen Gettings
💻

Justin
💻

Marcelo Alves
💻

Zorza
📖

This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!


import React from 'react'

import { notifyManager } from '../core/notifyManager'
import { noop, parseMutationArgs } from '../core/utils'
import { MutationObserver } from '../core/mutationObserver'
import { useQueryClient } from './QueryClientProvider'
import {
  UseMutateFunction,
  UseMutationOptions,
  UseMutationResult,
} from './types'
import {
  MutationFunction,
  MutationKey,
  MutationObserverResult,
} from '../core/types'

// HOOK

export function useMutation<
  TData = unknown,
  TError = unknown,
  TVariables = void,
  TContext = unknown
>(
  options: UseMutationOptions<TData, TError, TVariables, TContext>
): UseMutationResult<TData, TError, TVariables, TContext>
export function useMutation<
  TData = unknown,
  TError = unknown,
  TVariables = void,
  TContext = unknown
>(
  mutationFn: MutationFunction<TData, TVariables>,
  options?: UseMutationOptions<TData, TError, TVariables, TContext>
): UseMutationResult<TData, TError, TVariables, TContext>
export function useMutation<
  TData = unknown,
  TError = unknown,
  TVariables = void,
  TContext = unknown
>(
  mutationKey: MutationKey,
  options?: UseMutationOptions<TData, TError, TVariables, TContext>
): UseMutationResult<TData, TError, TVariables, TContext>
export function useMutation<
  TData = unknown,
  TError = unknown,
  TVariables = void,
  TContext = unknown
>(
  mutationKey: MutationKey,
  mutationFn?: MutationFunction<TData, TVariables>,
  options?: UseMutationOptions<TData, TError, TVariables, TContext>
): UseMutationResult<TData, TError, TVariables, TContext>
export function useMutation<
  TData = unknown,
  TError = unknown,
  TVariables = void,
  TContext = unknown
>(
  arg1:
    | MutationKey
    | MutationFunction<TData, TVariables>
    | UseMutationOptions<TData, TError, TVariables, TContext>,
  arg2?:
    | MutationFunction<TData, TVariables>
    | UseMutationOptions<TData, TError, TVariables, TContext>,
  arg3?: UseMutationOptions<TData, TError, TVariables, TContext>
): UseMutationResult<TData, TError, TVariables, TContext> {
  const options = parseMutationArgs(arg1, arg2, arg3)
  const queryClient = useQueryClient()

  // Create mutation observer
  const observerRef = React.useRef<
    MutationObserver<TData, TError, TVariables, TContext>
  >()
  const observer =
    observerRef.current || new MutationObserver(queryClient, options)
  observerRef.current = observer

  // Update options
  if (observer.hasListeners()) {
    observer.setOptions(options)
  }

  const [currentResult, setCurrentResult] = React.useState(() =>
    observer.getCurrentResult()
  )

  // Subscribe to the observer
  React.useEffect(
    () =>
      observer.subscribe(
        notifyManager.batchCalls(
          (
            result: MutationObserverResult<TData, TError, TVariables, TContext>
          ) => {
            // Check if the component is still mounted
            if (observer.hasListeners()) {
              setCurrentResult(result)
            }
          }
        )
      ),
    [observer]
  )

  const mutate = React.useCallback<
    UseMutateFunction<TData, TError, TVariables, TContext>
  >(
    (variables, mutateOptions) => {
      observer.mutate(variables, mutateOptions).catch(noop)
    },
    [observer]
  )

  if (currentResult.error && observer.options.useErrorBoundary) {
    throw currentResult.error
  }

  return { ...currentResult, mutate, mutateAsync: currentResult.mutate }
}


import React from 'react'
import axios from 'axios'

import {
  useQuery,
  useQueryClient,
  useMutation,
  QueryClient,
  QueryClientProvider,
} from 'react-query'
import { ReactQueryDevtools } from 'react-query/devtools'

const queryClient = new QueryClient()

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Example />
    </QueryClientProvider>
  )
}

function Example() {
  const queryClient = useQueryClient()
  const [text, setText] = React.useState('')

  const { status, data, error, isFetching } = useQuery('todos', async () => {
    const res = await axios.get('/api/data')
    return res.data
  })

  const addTodoMutation = useMutation(
    text => axios.post('/api/data', { text }),
    {
      // Optimistically update the cache value on mutate, but store
      // the old value and return it so that it's accessible in case of
      // an error
      onMutate: async text => {
        setText('')
        await queryClient.cancelQueries('todos')

        const previousValue = queryClient.getQueryData('todos')

        queryClient.setQueryData('todos', old => ({
          ...old,
          items: [...old.items, text],
        }))

        return previousValue
      },
      // On failure, roll back to the previous value
      onError: (err, variables, previousValue) =>
        queryClient.setQueryData('todos', previousValue),
      // After success or failure, refetch the todos query
      onSuccess: () => {
        queryClient.invalidateQueries('todos')
      },
    }
  )

  return (

        In this example, new items can be created using a mutation. The new item
        will be optimistically added to the list in hopes that the server
        accepts the item. If it does, the list is refetched with the true items
        from the list. Every now and then, the mutation may fail though. When
        that happens, the previous list of items is restored and the list is
        again refetched from the server.

      <form
        onSubmit={e => {
          e.preventDefault()
          addTodoMutation.mutate(text)
        }}
      >
        <input
          type="text"
          onChange={event => setText(event.target.value)}
          value={text}
        />
        <button>{addTodoMutation.isLoading ? 'Creating...' : 'Create'}</button>
      </form>

      {status === 'loading' ? (
        'Loading...'
      ) : status === 'error' ? (
        error.message
      ) : (
        <>

Updated At: {new Date(data.ts).toLocaleTimeString()}

            {data.items.map(datum => (

{datum}

            ))}

{isFetching ? 'Updating in background...' : ' '}

        </>
      )}
      <ReactQueryDevtools initialIsOpen />

  )
}


data: 10,
isFetching: false,
isSuccess: true,
isPreviousData: false,
})
// Switched query key
expect(states[2]).toMatchObject({
data: 10,
isFetching: false,

keep keep keep replace keep keep keep keep keep


# Example

To run this example:

- `npm install` or `yarn`
- `npm run dev` or `yarn dev`


// an endpoint for getting user info
export default (req, res) => {
  if (req.cookies['swr-test-token'] === 'swr') {
    // authorized
    res.json({
      loggedIn: true,
      name: 'Tanner',
      avatar: 'https://github.com/tannerlinsley.png',
    })
    return
  }

  res.json({
    loggedIn: false,
  })
}