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 behaviorssetupListeners(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.