React-Query is a powerful tool for fetching/caching the data on the frontend side, yet testing React-Query with Jest and React-testing-library might be a bit not straight-forward, in this article we’re going to demonstrate it and see how easily it can be.
Prerequisites
In this article we will be covering React-Query useInfiniteQuery hook with lazy loading data using InfiniteScroll library, along with the following testing tools:
- React-testing-library: UI testing utils.
- Jest: Testing framework.
Use Case
As we aren’t building a full-stack application in this article, we will use the DummyApi as our backend (you can navigate to their site and generate unique app-id), And we will create a simple list view that displays the user first name and last name with the ability to scroll down and fetch more data as the user scrolls down, until no more users data to fetch.
We will be covering testing both UI components and Custom hooks functionality.

Note: Full source code can be found on the following link:
Let’s get started!
Initial Setup
We will user Create-React-App to create the app, open your terminal and execute the following command:
npx create-react-app testing-react-query-components --template typescript
Now move to the project directory:
cd testing-react-query-components
We need to install the rest of the libraries, so execute the following command inside the project directory, which will install react-query along with react-infinite-scroll-component:
yarn add react-infinite-scroll-component react-query
Let’s as well add the testing dev dependencies, execute the following command to install them:
yarn add @testing-library/jest-dom @testing-library/react @testing-library/user-event @types/jest
Now all of the dependencies should be installed, let’s start coding!
Inside index.tsx file, let’s add the following code:
import { render } from "react-dom";
import { QueryClient, QueryClientProvider } from "react-query";
import App from "./App";
import './index.css'
const queryClient = new QueryClient();
const rootElement = document.getElementById("root");
render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>,
rootElement
);
At the above file, we’re simply initializing a new QueryClient client and wrapping the App component with it, we will get later to the App component, now let’s build the logic!
Create a new types.d.ts file which will contain the typescript declarations
export type User = {
firstName: string;
lastName: string;
};
export type UsersPage = {
results: User[];
next: number | undefined;
};
The User type, simply represents the user first and last name and the UsersPage type will represent the response that we’re getting from the server, in which we will have an array/list of User and number next to indicate if there are any more users data to fetch.
useInfiniteQuery custom hook implementation
import { useInfiniteQuery } from 'react-query';
import { UsersPage } from './types';
async function getData({ pageParam = 0 }) {
const response = await fetch(`https://dummyapi.io/data/v1/user?page=${pageParam}&limit=50`, {
headers: {
'app-id': '62f43477f19452557ba1ce76',
},
});
if (!response.ok) {
throw new Error('Problem fetching data');
}
const dataFromServer = await response.json();
const data: UsersPage = {
results: dataFromServer.data,
next:
dataFromServer.total > dataFromServer.page * dataFromServer.limit
? pageParam + 1
: undefined,
};
return data;
}
export const useUsersQuery = () => {
const query = useInfiniteQuery<UsersPage, Error>('users', getData, {
getNextPageParam: (lastPage) => lastPage.next,
});
return query;
};
getData is responsible on fetching the users data from dummyapi.io using the browser fetch api, and it does check if the response is not valid/ok (line:10) we throw an error with the message Problem fetching data.
If the response is valid, we convert the response to json, using the method response.json(), then we create an object of type UsersPage that will include an array of users results (line:16), and a number stored in the next property (line: 17), that indicates if more results needs to be fetched.
The logic on identifying if there are more results to fetch from the backend or not is done by checking the total results we receive from response is greater than page size multiplied by the limit, if it’s greater then we increment the page number by 1, if not we simply return undefined which React-query will use internally to stop fetching/paginating the request.
Here’s an example of that:
On the first request, we issue the following request
https://dummyapi.io/data/v1/user?page=0&limit=50
note that the page number is 0 and the limit is 50, is response to that we receive the following respoonse:
{
"data": [
{
"id": "60d0fe4f5311236168a109ca",
"title": "ms",
"firstName": "Sara",
"lastName": "Andersen",
"picture": "https://randomuser.me/api/portraits/women/58.jpg"
},
...up to 49 another object
],
"total": 99,
"page": 0,
"limit": 50
}
note that the total number of results is 99 and the page number is 0 and the limit is 50, running our calculations
total > page * limit
99 > 0 * 50, which will results in true, so we increment the page number by 1.
In the next request the api url will be
https://dummyapi.io/data/v1/user?page=1&limit=50
Note that the page number is changed from 0 to 1, and so on…
useUsersQuery custom hook, is simply a calling React-query useInfiniteQuery, first we’re setting the query key to users, and we’re passing the getData function as a 2nd parameter, in the last parameter, we’re passing an object that contains function definition for the getNextPageParam property, we simply returning the next page number, or undefined!
So this so far the logic needed to fetch the user data from the backend and paginate through the pages, until no more data to fetch.
Now moving to the UI part, create/edit the App.tsx file as the following:
import React from 'react';
import { useUsersQuery } from './useInfiniteQuery';
import InfiniteScroll from 'react-infinite-scroll-component';
import { User } from './types';
export default function App() {
const { data, error, fetchNextPage, hasNextPage, status } = useUsersQuery();
if (status === 'loading') {
return <div>Loading users...</div>;
}
if (status === 'error') {
return <div>{error?.message}</div>;
}
if (data === undefined) {
return <div>No users data found!</div>;
}
const dataLength = data.pages.reduce((counter, page) => {
return counter + page.results.length;
}, 0);
return (
<div style={{ margin: '20px' }}>
<h1>Users List</h1>
<div style={{ border: '1px solid #ccc', height: '400px', overflow: 'scroll' }} id="scrollbar-target">
<InfiniteScroll
dataLength={dataLength}
next={fetchNextPage}
hasMore={!!hasNextPage}
loader={<p style={{ color: 'deepskyblue', fontSize: '20px', textAlign: 'center' }}><b>Loading...</b></p>}
data-testid="infinite-scroll"
scrollableTarget="scrollbar-target"
style={{ padding: '5px' }}
endMessage={
<p style={{ textAlign: 'center' }}>
<b>Yay! You have seen it all</b>
</p>
}
>
{data.pages.map((group, i) => (
<React.Fragment key={i}>
{group.results.map((user: User) => (
<p key={user.firstName + user.lastName}>
{user.firstName} {user.lastName}
</p>
))}
</React.Fragment>
))}
</InfiniteScroll>
</div>
</div>
);
}
Here’s the description of the properties we’re destructuring from useUsersQuery hooks:
- data: It will include the users pages array, and in each page we will have an array of users (remember in the file types.d.ts we declared UsersPage type).
- error: In case of any error happened in the custom hook/api error, it will be stored in this variable and we show that in the screen in case.
- status: This can hold many states, for now we’re only checking the loading/error statuses.
- fetchNextPage: This will be called in case we have more results to fetch from the backend.
- hasNextPage: A boolean value indicates if we have another page to fetch or not.
At Line:18, note we are calculating the size of the data that we have so far, this will help InfiniteScroll component to decide in calling the next function or not.
That’s all for making our app works, now running the app by executing the following command from the terminal
yarn start
A new page should open in the browser and you should see the following list of users, As you scroll down in the list, you will notice the Loading… message, while fetching the next page of data from the server, till you reach to the end of the results, as shown below!

Components testing
Looking at the App.tsx component, we can see that it does a few things:
- Calls
useUsersQuery
hook. - Renders a loading message when
status === 'loading'
. - Renders an error message when
.status
=== 'error' - Renders
No users data found
whendata === undefined
- Renders the component data when it’s available.
Therefore, we need to test all of these cases.
Our tests will rely on jest.mock
which in turn will allow us to mock the implementation of the useUsersQuery
. This avoids wrapping the component with ReactQueryProvider
as we will be testing the component behavior and not the custom react-query
hook itself.
Let’s start testing case by case.
Create a new folder under the src
directory for tests, name it __tests__
, and inside the new __tests__
folder create a new file called App.test.tsx
, also make sure to import @testing-library/jest-dom
inside the src/setupTests.ts
file
import "@testing-library/jest-dom";
1- Testing the app renders correctly without crashing
import React from "react";
import { render, screen } from "@testing-library/react";
import { useUsersQuery } from "../useInfiniteQuery";
import App from "../App";
// Make TypeScript Happy, by resolving TS errors
const mockedUseUsersQuery = useUsersQuery as jest.Mock<any>;
// Mock the hook module
jest.mock("../useInfiniteQuery");
describe("<App />", () => {
beforeEach(() => {
mockedUseUsersQuery.mockImplementation(() => ({ isLoading: true }));
});
afterEach(() => {
jest.clearAllMocks();
});
it("Renders without crashing", () => {
render(<App />);
});
});
run the command yarn test
from the terminal, and you should see the test is passing

2- Adding test for the loading state
import React from "react";
import { render, screen } from "@testing-library/react";
import { useUsersQuery } from "../useInfiniteQuery";
import App from "../App";
// Make TypeScript Happy, by resolving TS errors
const mockedUseUsersQuery = useUsersQuery as jest.Mock<any>;
// Mock the hook module
jest.mock("../useInfiniteQuery");
describe("<App />", () => {
beforeEach(() => {
mockedUseUsersQuery.mockImplementation(() => ({ isLoading: true }));
});
afterEach(() => {
jest.clearAllMocks();
});
it("Renders without crashing", () => {
render(<App />);
});
it("Displays loading message", () => {
mockedUseUsersQuery.mockImplementation(() => ({
status: 'loading',
}));
render(<App />);
expect(screen.getByText(/Loading users.../i)).toBeInTheDocument()
});
});

3- Testing error state
import React from "react";
import { render, screen } from "@testing-library/react";
import { useUsersQuery } from "../useInfiniteQuery";
import App from "../App";
// Make TypeScript Happy, by resolving TS errors
const mockedUseUsersQuery = useUsersQuery as jest.Mock<any>;
// Mock the hook module
jest.mock("../useInfiniteQuery");
describe("<App />", () => {
beforeEach(() => {
mockedUseUsersQuery.mockImplementation(() => ({ isLoading: true }));
});
afterEach(() => {
jest.clearAllMocks();
});
it("Renders without crashing", () => {
render(<App />);
});
it("Displays loading message", () => {
mockedUseUsersQuery.mockImplementation(() => ({
status: 'loading',
}));
render(<App />);
expect(screen.getByText(/Loading users.../i)).toBeInTheDocument()
});
it("Displays error message", () => {
mockedUseUsersQuery.mockImplementation(() => ({
status: 'error',
error: {
message: 'An error occured!'
}
}));
render(<App />);
expect(screen.getByText(/An error occured!/i)).toBeInTheDocument()
});
});

4- Testing no data found case
import React from "react";
import { render, screen } from "@testing-library/react";
import { useUsersQuery } from "../useInfiniteQuery";
import App from "../App";
// Make TypeScript Happy, by resolving TS errors
const mockedUseUsersQuery = useUsersQuery as jest.Mock<any>;
// Mock the hook module
jest.mock("../useInfiniteQuery");
describe("<App />", () => {
beforeEach(() => {
mockedUseUsersQuery.mockImplementation(() => ({ isLoading: true }));
});
afterEach(() => {
jest.clearAllMocks();
});
it("Renders without crashing", () => {
render(<App />);
});
it("Displays loading message", () => {
mockedUseUsersQuery.mockImplementation(() => ({
status: 'loading',
}));
render(<App />);
expect(screen.getByText(/Loading users.../i)).toBeInTheDocument()
});
it("Displays error message", () => {
mockedUseUsersQuery.mockImplementation(() => ({
status: 'error',
error: {
message: 'An error occured!'
}
}));
render(<App />);
expect(screen.getByText(/An error occured!/i)).toBeInTheDocument()
});
it("Displays no data found message", () => {
mockedUseUsersQuery.mockImplementation(() => ({
status: 'loaded',
data: undefined
}));
render(<App />);
expect(screen.getByText(/No users data found!/i)).toBeInTheDocument()
});
});

5- Testing rendering the component when we receive the data from the backend, for this case let’s create a new fixtures
file under the src
directory, that will contain the mocked response per page, full source code can be found on the following link

Now, let’s get back to the test file and add the 5th case
import React from "react";
import { render, screen } from "@testing-library/react";
import { useUsersQuery } from "../useInfiniteQuery";
import App from "../App";
import { responseForPage0 } from "../fixtures";
// Make TypeScript Happy, by resolving TS errors
const mockedUseUsersQuery = useUsersQuery as jest.Mock<any>;
// Mock the hook module
jest.mock("../useInfiniteQuery");
describe("<App />", () => {
beforeEach(() => {
mockedUseUsersQuery.mockImplementation(() => ({ isLoading: true }));
});
afterEach(() => {
jest.clearAllMocks();
});
it("Renders without crashing", () => {
render(<App />);
});
it("Displays loading message", () => {
mockedUseUsersQuery.mockImplementation(() => ({
status: 'loading',
}));
render(<App />);
expect(screen.getByText(/Loading users.../i)).toBeInTheDocument()
});
it("Displays error message", () => {
mockedUseUsersQuery.mockImplementation(() => ({
status: 'error',
error: {
message: 'An error occured!'
}
}));
render(<App />);
expect(screen.getByText(/An error occured!/i)).toBeInTheDocument()
});
it("Displays no data found message", () => {
mockedUseUsersQuery.mockImplementation(() => ({
status: 'loaded',
data: undefined
}));
render(<App />);
expect(screen.getByText(/No users data found!/i)).toBeInTheDocument()
});
it("Displays the users list", () => {
mockedUseUsersQuery.mockImplementation(() => ({
status: 'success',
data: {
pages: [{ results: responseForPage0.data, next: 1 }]
}
}));
render(<App />);
expect(screen.getByText(`${responseForPage0.data[0].firstName} ${responseForPage0.data[0].lastName}`)).toBeInTheDocument()
});
});
And by running that our test file should 100% covered and should looks green!

That’s it for using useInfiniteQuery hook and InfiniteScroll component, with implementing 100% test coverage for all of the use cases.
Please note that for testing the custom hook functionalities you can mock the browser Fetch API, and that would work, but it won’t support pagination, which means you won’t be able to cover the functionality 100%, a following up article will be made to configure the app with Axios HTTP client, and implement 100% test coverage for the useUserQuery custom hook.
And as always, happy coding! 🥳👋🏻
Photo from unsplash