React has become a dominant force in front-end development due to its component-based architecture, making it easier to build complex user interfaces. However, managing state, especially when dealing with data fetching, caching, and synchronization, can be challenging. This is where Redux Toolkit (RTK) comes into play, offering a streamlined way to manage state in React applications. One of the powerful tools included in Redux Toolkit is RTK Query, a data fetching and caching solution that greatly simplifies the process of making API calls in React.

This article will explore how to use RTK Query to handle API calls in a React application. We’ll cover the basics, setting up RTK Query, making API requests, managing state, and handling errors. By the end, you’ll have a solid understanding of how RTK Query can improve your data-fetching strategies in React.

What is RTK Query?

RTK Query is an advanced data-fetching and caching tool built into Redux Toolkit. It provides a powerful set of utilities for managing API calls, which include automatic caching, request deduplication, and background data synchronization. Unlike traditional Redux, where you have to manually create actions, reducers, and thunks to manage asynchronous operations, RTK Query abstracts much of this complexity away, allowing you to focus on building features.

Key Features of RTK Query

  • Declarative API Calls: RTK Query allows you to define endpoints for your API calls declaratively.
  • Automatic Caching: Responses from API calls are cached automatically, reducing the number of unnecessary requests.
  • Optimistic Updates: RTK Query supports optimistic updates, improving the user experience by updating the UI before the API request is confirmed.
  • Auto Re-fetching: It can automatically re-fetch data in the background to keep your UI up to date.

Setting Up RTK Query in a React Project

To use RTK Query, you first need to set up Redux Toolkit in your React project. If you haven’t already done so, you can install Redux Toolkit and React-Redux using npm or yarn:

bash

npm install @reduxjs/toolkit react-redux

Setting Up the Store

Next, create a Redux store using configureStore from Redux Toolkit. Here’s an example:

javascript

import { configureStore } from '@reduxjs/toolkit';
import { setupListeners } from '@reduxjs/toolkit/query';
import { apiSlice } from './features/api/apiSlice';
export const store = configureStore({
reducer: {
// Add the api reducer to the store
[apiSlice.reducerPath]: apiSlice.reducer,
},
// Adding the api middleware enables caching, invalidation, polling, and other features of RTK Query
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(apiSlice.middleware),
});// Enable refetchOnFocus/refetchOnReconnect behaviors
setupListeners(store.dispatch);

In this example, we configure our Redux store with the reducer and middleware from our RTK Query slice (which we’ll define shortly). The setupListeners function enables automatic re-fetching when the user’s browser window regains focus or reconnects to the internet.

Creating an API Slice

An API slice in RTK Query is where you define all the endpoints for your API calls. Let’s create an API slice to fetch a list of users from a REST API:

javascript

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

export const apiSlice = createApi({
reducerPath: ‘api’,
baseQuery: fetchBaseQuery({ baseUrl: ‘https://jsonplaceholder.typicode.com’ }),
endpoints: (builder) => ({
getUsers: builder.query({
query: () => ‘/users’,
}),
}),
});

export const { useGetUsersQuery } = apiSlice;

In this example, we define an API slice using createApi, specify a baseUrl, and then define an endpoint called getUsers. This endpoint fetches data from the /users path. The useGetUsersQuery hook is automatically generated by RTK Query for use in our components.

Using RTK Query in React Components

Now that our API slice is set up, we can use the useGetUsersQuery hook in a React component to fetch and display data.

Fetching Data

Here’s an example of a simple component that fetches and displays a list of users:

javascript

import React from 'react';
import { useGetUsersQuery } from './features/api/apiSlice';
const UsersList = () => {
const { data: users, error, isLoading } = useGetUsersQuery();if (isLoading) return <div>Loading…</div>;
if (error) return <div>Error occurred: {error.message}</div>;return (
<div>
<h2>Users List</h2>
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>

);
};export default UsersList;

In this component, the useGetUsersQuery hook is used to fetch user data. This hook returns several properties, including:

  • data: The fetched data.
  • error: An error object if the request failed.
  • isLoading: A boolean indicating whether the request is in progress.

Depending on these states, the component displays a loading indicator, an error message, or the list of users.

Handling Errors and Loading States

RTK Query provides built-in loading and error states, making it straightforward to handle different scenarios in your UI. In the above example, we used isLoading and error to conditionally render content. RTK Query’s caching mechanism also helps by preventing unnecessary loading spinners when data is already available in the cache.

Optimistic Updates

Optimistic updates allow you to update the UI immediately after an action, rather than waiting for the server response. This improves the user experience by making the application feel more responsive.

Here’s how you might implement an optimistic update when adding a new user:

javascript

import { apiSlice } from './apiSlice';

export const useAddUserMutation = apiSlice.injectEndpoints({
endpoints: (builder) => ({
addUser: builder.mutation({
query: (newUser) => ({
url: ‘/users’,
method: ‘POST’,
body: newUser,
}),
// Perform an optimistic update
onQueryStarted: async (newUser, { dispatch, queryFulfilled }) => {
try {
await queryFulfilled;
} catch (err) {
// Undo the optimistic update if the mutation fails
dispatch(apiSlice.util.updateQueryData(‘getUsers’, undefined, draft => {
draft.push(newUser);
}));
}
},
}),
}),
});

export const { useAddUserMutation } = apiSlice;

In this example, the addUser mutation performs an optimistic update by immediately adding the new user to the list of users before the API request is completed. If the request fails, the update is reverted.

Caching and Cache Invalidation

One of RTK Query’s standout features is its automatic caching of API responses. When you make a request, RTK Query stores the response in the cache, and subsequent requests for the same data will return the cached version unless explicitly invalidated.

To manually invalidate the cache, you can define invalidation behaviors within your endpoint definitions. Here’s an example:

javascript

export const apiSlice = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: 'https://jsonplaceholder.typicode.com' }),
tagTypes: ['User'],
endpoints: (builder) => ({
getUsers: builder.query({
query: () => '/users',
providesTags: ['User'],
}),
addUser: builder.mutation({
query: (newUser) => ({
url: '/users',
method: 'POST',
body: newUser,
}),
invalidatesTags: ['User'],
}),
}),
});
export const { useGetUsersQuery, useAddUserMutation } = apiSlice;

In this example, the getUsers query is tagged with a 'User' tag. The addUser mutation invalidates this tag, causing the getUsers query to re-fetch the updated data.

Advanced Usage

Polling Data

Polling is useful when you need to periodically fetch the latest data. RTK Query allows you to easily set up polling on any query:

javascript

const { data: users, error, isLoading } = useGetUsersQuery(undefined, {
pollingInterval: 60000, // Poll every 60 seconds
});

This snippet configures the useGetUsersQuery hook to poll for new data every 60 seconds.

Pagination

Handling pagination with RTK Query is straightforward. You can define your endpoints to accept parameters, such as page numbers:

javascript

export const apiSlice = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: 'https://jsonplaceholder.typicode.com' }),
endpoints: (builder) => ({
getUsers: builder.query({
query: (page = 1) => `/users?_page=${page}`,
}),
}),
});
export const { useGetUsersQuery } = apiSlice;

Now, in your component, you can call the useGetUsersQuery hook with a specific page number:

javascript

const { data: users, error, isLoading } = useGetUsersQuery(2); // Fetch page 2

This approach allows you to easily manage pagination in your components.

Injecting Endpoints

RTK Query supports dynamically injecting endpoints after the API slice has been created. This is useful for large applications where different slices might need to inject endpoints independently:

javascript

import { apiSlice } from './apiSlice';

export const extendedApiSlice = apiSlice.injectEndpoints({
endpoints: (builder) => ({
getUserById: builder.query({
query: (id) => `/users/${id}`,
}),
}),
});

export const { useGetUserByIdQuery } = extendedApiSlice;

This method extends the existing API slice with a new endpoint without modifying the original slice.

Conclusion

RTK Query is a powerful addition to Redux Toolkit that simplifies data fetching and state management in React applications. Its features, such as automatic caching, optimistic updates, and request deduplication, reduce boilerplate code and improve the efficiency and maintainability of your applications.

By using RTK Query, developers can focus more on building features rather than worrying about the complexities of managing API calls and state synchronization. Whether you’re working on a small project or a large-scale application, integrating RTK Query into your React workflow will undoubtedly enhance your development experience.

With its declarative approach, advanced caching mechanisms, and seamless integration with Redux, RTK Query offers a modern and efficient way to manage API interactions in your React applications. If you haven’t tried it yet, it’s definitely worth exploring as it can significantly improve both developer productivity and application performance.