How I fetch data in React

I have used multiple methods for fetching data in React applications, from the traditional fetch API to creating a custom Axios client. While these methods do the job, they often require additional boilerplate for caching, state management, and error handling. Nothing, however, comes close to the elegance and simplicity of Redux Toolkit Query (RTK Query).
Setting Up Redux Toolkit Query
I typically organize my codebase by creating a dedicated store folder inside the src directory. This folder is the central location for all Redux slices and API logic, making the application structure clean, modular, and easy to maintain.
src/
|- store/
|- index.ts // Root store configuration
|- api/
|- index.ts // Centralized export for API slices
|- [model]/ // Separate folder for each model (e.g., posts, users, etc.)
|- queries.ts // Contains query definitions for the model
|- mutations.ts // Contains mutation definitions for the model
store/
This folder is dedicated to configuring and managing the Redux store.
Key Points:
- Purpose: Centralized store configuration and middleware integration.
- Contents:
index.ts
: The root file where the Redux store is configured.
Middleware like api.middleware
from RTK Query is integrated here.
api/
The core folder for all API-related configurations and logic.
index.ts
A centralized export file for all API slices. This allows you to consolidate and manage multiple slices in one place.
[model]/
A subfolder for each domain model (e.g., posts
, users
, products
). Each model gets its folder for modularity, making the application easier to scale as new models are added.
Example Structure:
api/
|- posts/
|- queries.ts
|- mutations.ts
|- users/
|- queries.ts
|- mutations.ts
[model]/queries.ts
This folder contains query definitions for fetching data related to the specific model. Queries are created using RTK Query’s builder.query
method.
Example: posts/queries/index.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const apiSlice = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts',
}),
getPostById: builder.query({
query: (id) => `/posts/${id}`,
}),
}),
});
export const { useGetPostsQuery, useGetPostByIdQuery } = apiSlice;
[model]/mutations.ts
This folder contains mutation definitions for creating, updating, or deleting data related to the model. Mutations are defined using RTK Query’s builder.mutation
method.
Example: posts/mutations/index.ts
import { apiSlice } from '../queries';
export const enhancedApiSlice = apiSlice.injectEndpoints({
endpoints: (builder) => ({
createPost: builder.mutation({
query: (newPost) => ({
url: '/posts',
method: 'POST',
body: newPost,
}),
}),
deletePost: builder.mutation({
query: (id) => ({
url: `/posts/${id}`,
method: 'DELETE',
}),
}),
}),
});
export const { useCreatePostMutation, useDeletePostMutation } = enhancedApiSlice;
Example Usage in a Component
Here’s how you can use this setup in a React component:
import React from 'react';
import { useGetPostsQuery, useCreatePostMutation } from '../api/post/queries';
import { useGetPostsQuery, useCreatePostMutation } from '../api';
const PostsList = () => {
const { data: posts, isLoading, error } = useGetPostsQuery();
const [createPost] = useCreatePostMutation();
const handleCreatePost = async () => {
await createPost({ title: 'New Post', content: 'Post content' });
};
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error loading posts</p>;
return (
<div>
<button onClick={handleCreatePost}>Add Post</button>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
};
export default PostsList;
Advantages of This Setup
- Modularity:
- Separating queries and mutations into individual files within model folders ensures each model is self-contained and easier to manage.
- Scalability:
- Adding new models is as simple as creating a new folder under
api/
and defining its queries and mutations.
- Adding new models is as simple as creating a new folder under
- Centralized Store Configuration:
- All slices are connected in
store/index.ts
, maintaining a single source of truth for the application state.
- All slices are connected in
- Reusability:
- Queries and mutations are exported as reusable hooks, making them easy to use in multiple components.
- Clean and Readable Imports:
- By centralizing exports in
api/index.ts
, you can avoid long and repetitive import paths.
- By centralizing exports in
Type-Safe Queries and Mutations with RTK Query
TypScript interfaces ensure type safety for queries and mutations by enhancing code quality, minimizing runtime errors, and providing a better developer experience through auto-completion and error detection.
Using Interfaces for Type Safety
Defining TypeScript interfaces for API responses and request bodies helps ensure that your queries and mutations are properly typed, maintaining consistent data structures throughout your application.
Example: Type-Safe Queries
Define an interface for your API response and use it in your query.
interface Post {
id: number;
title: string;
content: string;
}
export const apiSlice = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
getPosts: builder.query<Post[], void>({ // Post[]: Array of Post, void: no argument
query: () => '/posts',
}),
getPostById: builder.query<Post, number>({ // Post: single Post, number: post ID argument
query: (id) => `/posts/${id}`,
}),
}),
});
export const { useGetPostsQuery, useGetPostByIdQuery } = apiSlice;
Automating Types with Code Generation
For large or complex APIs, manually defining types can be time-consuming and error-prone. I like to use tools like OpenAPI Generator and GraphQL Code Generator to automate this process by generating TypeScript types directly from API schemas. This ensures that your types stay in sync with your backend, providing consistency and reducing boilerplate. These generated types can seamlessly integrate into your RTK Query slices for type-safe queries and mutations, streamlining your development workflow.
Conclusion
In my professional experience, I have explored various tools and strategies for data fetching in React, but none compare to the simplicity and efficiency of Redux Toolkit Query (RTK Query). Its modular structure, centralized store configuration, and reusable hooks make it an ideal solution for easily managing state and API logic.
TypeScript interfaces ensure consistent and predictable data handling, by eliminating runtime errors and enhancing developer productivity through autocompletion and type checking.
RTK Query also simplifies state management by addressing common scenarios like loading, error, and success states. Furthermore, its built-in mechanisms for revalidating stale data ensure that your application consistently displays fresh and accurate information without making unnecessary API calls. This makes it ideal for real-world applications.
Lastly, RTK Query streamlines the implementation of advanced features, including authenticated requests, which I plan to explore in a future post. For now, this setup has become my go-to solution for building scalable, type-safe, and maintainable React applications.