TanStack Query: Data Fetching meme and Theodore Negusu, PM, Frontend Developer, UI/UX Designer

Building Scalable and Efficient Data Fetching with TanStack Query

Theodore Negusu
10 min readJun 1, 2024

Fetching data from a server is crucial for most web applications. In React, there are various ways to do this, each with its own benefits and use cases. This guide will walk you through three common methods: using the fetch API with useEffect and useState hooks, using Axios with useEffect and useState hooks, and using React Query. Finally, we'll introduce a comprehensive structure for API requests and state management that enhances scalability and maintainability. let’s get started.

1. Using the fetch API with useEffect and useState Hooks

The fetch API is a built-in browser API for making HTTP requests. Combined with React's useEffect and useState hooks, it's a straightforward way to fetch data.

Step-by-Step Guide

  1. Initialize State: Use the useState hook to create state variables for storing data, loading status, and error messages.
  2. Fetch Data: Use the useEffect hook to fetch data when the component mounts.
  3. Handle Responses: Process the server’s response, updating state variables accordingly.

Example

import React, { useEffect, useState } from 'react';

const FetchDataComponent = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
fetch('https://api.example.com/data')
.then((response) => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then((data) => {
setData(data);
setLoading(false);
})
.catch((error) => {
setError(error);
setLoading(false);
});
}, []);

if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;

return (
<div>
{data && data.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
};

export default FetchDataComponent;

Pros and Cons

Pros:

  • Built-in and doesn’t require additional libraries.
  • Simple to set up for basic use cases.

Cons:

  • unstructured error handling compared to other libraries.
  • Lacks advanced features like automatic retries and caching.

2. Using Axios with useEffect and useState Hooks

Axios is a popular promise-based HTTP client for JavaScript that offers a cleaner and more powerful API than the fetch API. It simplifies the process of making HTTP requests and handling responses.

Step-by-Step Guide

  1. Install Axios: First, install Axios using npm or yarn.
pnpm i axios
# or
npm install axios

2. Initialize State: Similar to the fetch example, use useState to create state variables for data, loading status, and error messages.

3. Fetch Data with Axios: Use the useEffect hook to fetch data using Axios when the component mounts.

Example

import React, { useEffect, useState } from 'react';
import axios from 'axios';

const AxiosDataComponent = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
axios.get('https://api.example.com/data')
.then((response) => {
setData(response.data);
setLoading(false);
})
.catch((error) => {
setError(error);
setLoading(false);
});
}, []);

if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;

return (
<div>
{data && data.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
};

export default AxiosDataComponent;

Error Handling and Edge Cases

  • Network Errors: Axios automatically catches network errors and provides detailed error messages.
  • Response Interceptors: Axios allows setting up interceptors to handle responses and errors globally.

Pros and Cons

Pros:

  • Cleaner and more concise syntax than fetch.
  • Built-in support for request and response interceptors.
  • Better error handling and automatic JSON parsing.

Cons:

  • Requires installing an additional library.
  • Slightly larger bundle size compared to fetch.

3. Using React Query

Tanstack Query is a powerful library that makes fetching, caching, and updating data easier. It handles many aspects of data fetching out of the box, such as caching, background updates, and retries.

Step-by-Step Guide

  1. Install React Query: First, install React Query using npm or yarn.
pnpm i @tanstack/react-query
# or
npm install @tanstack/react-query

2. Set Up React Query Provider: Wrap your application with the QueryClientProvider.

3. Create Query Functions: Define functions for fetching data.

4. Use React Query Hooks: Use React Query hooks to fetch and manage data in your components.

Example

import React from 'react';
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
import axios from 'axios';

const queryClient = new QueryClient();

const fetchData = async () => {
const { data } = await axios.get('https://api.example.com/data');
return data;
};

const ReactQueryDataComponent = () => {
const { data, error, isLoading } = useQuery('data', fetchData);

if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;

return (
<div>
{data && data.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
};

const App = () => (
<QueryClientProvider client={queryClient}>
<ReactQueryDataComponent />
</QueryClientProvider>
);

export default App;

Pros and Cons

Pros:

  • Automatic caching and background updates.
  • Simplified data fetching and state management.
  • Rich features like retries, pagination, and infinite scrolling.

Cons:

  • Requires learning a new library.
  • Adds complexity to simple use cases.

4. Comprehensive Structure for API Requests and State Management

For larger projects, it’s helpful to have a well-organized structure for handling API requests and state management. This structure will use React Query along with a centralized file for API functions and error handling.

This approach, inspired by Beka, ensures your application can handle complex state and data-fetching requirements in a clean and efficient manner.

Step-by-Step Guide

  1. Create API Function Files: Define functions for making API requests.
  2. Define Custom Hooks: Create custom hooks using React Query for each API request.
  3. Centralize API Handlers: Aggregate all API functions and hooks in a central file.
  4. Use API Handlers in Components: Import and use the centralized API handlers in your React components.

Example: Blog API

Step 1. Centralized Query Client Configuration

First, ensure you have a centralized query client configuration. Create a file queryClient.ts:

import { QueryClient } from '@tanstack/react-query';

const queryClient = new QueryClient();

export default queryClient;

Step 2: Create API Function File

Create a file /util/api/blog.ts for handling blog-related API requests:

import axios from 'axios';
import { FileType, ErrorRes, SuccessRes } from '../../types/core';
// ==============================
// interface and type definitions
// ==============================
export interface BlogGallery {
url: string;
id: string;
}

export interface BlogUpdate {
title: string;
description: string;
date: string;
}

export interface BlogType extends BlogFormType {
id: string;
authorId?: string;
status: 'DRAFT' | 'PENDING' | 'APPROVED' | 'REJECTED' | 'TRASH';
approvedAt: string;
createdAt: string;
__v: number;
author: {
fullName: string;
avatar: FileType;
id: string;
};
}

export interface BlogFormType {
title: string;
description: string;
files: FileType;
}

// Add more Interfaces and types as needed

// ========================
// API Function definitions
// ========================

export async function createBlogFn(data: BlogFormType) {
return (await axios.post('/api/blog/create', data)).data;
}

export async function deleteBlogFn(id: string) {
return (await axios.delete('/api/blog/delete/' + id)).data;
}

export async function updateBlogFn(data: BlogFormType, id: string) {
return (await axios.put('/api/blog/update/' + id, data)).data;
}

export async function myBlogsFn() {
return (await axios.get('/api/blog/myBlog')).data;
}

export async function getBlogByIdFn(id: string) {
return (await axios.get('/api/blog/show/' + id)).data[0];
}

export async function getBlogFn() {
return (await axios.get('/api/blog/get/')).data;
}

export async function requestForApprovalFn(id: string) {
return (await axios.post('/api/blog/request/' + id)).data;
}

export async function approveBlogFn(id: string) {
return (await axios.post('/api/blog/approve/' + id)).data;
}

export async function rejectBlogFn(id: string) {
return (await axios.post('/api/blog/reject/' + id)).data;
}

export async function unpublishBlogFn(id: string) {
return (await axios.patch('/api/blog/unpublish/' + id)).data;
}

// Add more API functions as needed

Step 3. React Query Hooks with Error Handling and Toasting

In the same /util/api/blog.ts file, define your hooks using React Query:

import Toaster from '../../components/Toast/Toast';
import Router from 'next/router';
import {
UseMutationOptions,
UseQueryOptions,
useMutation,
useQuery,
} from '@tanstack/react-query';
import { AxiosError } from 'axios';
import queryClient from '../queryClient';
import {
createBlogFn,
deleteBlogFn,
updateBlogFn,
myBlogsFn,
getBlogByIdFn,
getBlogFn,
requestForApprovalFn,
approveBlogFn,
rejectBlogFn,
unpublishBlogFn,
BlogType,
BlogFormType,
SuccessRes,
ErrorRes,
} from './blog';

const Blog = {
MyBlog: {
useQuery: (options?: UseQueryOptions<BlogType[], AxiosError<ErrorRes>>) =>
useQuery({
queryKey: ['MyBlog'],
queryFn: () => myBlogsFn(),
...options,
}),
},
GetBlogById: {
useQuery: (
id: string,
options?: UseQueryOptions<BlogType, AxiosError<ErrorRes>>
) =>

useQuery({
queryKey: ['MyBlog', id],
queryFn: () => getBlogByIdFn(id),
...options,
}),
},
GetBlog: {
useQuery: (options?: UseQueryOptions<BlogType[], AxiosError<ErrorRes>>) =>
useQuery({
queryKey: ['MyBlog'],
queryFn: () => getBlogFn(),
...options,
}),
},
UpdateBlog: {
useMutation: (
id: string,
options?: UseMutationOptions<
SuccessRes,
AxiosError<ErrorRes>,
BlogFormType
>
) =>

useMutation({
...options,
mutationFn: (data) => updateBlogFn(data, id),
onSuccess: (data) => {
Toaster.closeLoading('LOADING');
Toaster.openSuccess(data.message);
queryClient.invalidateQueries({ queryKey: ['MyBlog', id] });
Router.push('/dashboard/blog/');
},
onError: (error) => {
Toaster.closeLoading('LOADING');
Toaster.openError(error.response?.data.message as string);
},
onMutate: (variables) => {
Toaster.openLoading('Please wait');
},
}),
},
CreateBlog: {
useMutation: (
options?: UseMutationOptions<
SuccessRes,
AxiosError<ErrorRes>,
BlogFormType
>
) =>

useMutation({
...options,
mutationFn: (data) => createBlogFn(data),
onSuccess: (data) => {
Toaster.closeLoading('LOADING');
Toaster.openSuccess(data.message);
queryClient.invalidateQueries({ queryKey: ['MyBlog'] });
Router.push('/dashboard/blog/');
},
onError: (error) => {
Toaster.closeLoading('LOADING');
Toaster.openError(error.response?.data.message as string);
},
onMutate: (variables) => {
Toaster.openLoading('Please wait');
},
}),
},
DeleteBlog: {
useMutation: (
options?: UseMutationOptions<SuccessRes, AxiosError<ErrorRes>, string>
) =>

useMutation({
...options,
mutationFn: (id) => deleteBlogFn(id),
onSuccess: (data) => {
Toaster.closeLoading('LOADING');
Toaster.openSuccess(data.message);
queryClient.invalidateQueries({ queryKey: ['MyBlog'] });
},
onError: (error) => {
Toaster.closeLoading('LOADING');
Toaster.openError(error.response?.data.message as string);
},
onMutate: (variables) => {
Toaster.openLoading('Please wait');
},
}),
},
RejectBlog: {
useMutation: (
options?: UseMutationOptions<SuccessRes, AxiosError<ErrorRes>, string>
) =>

useMutation({
...options,
mutationFn: (id) => rejectBlogFn(id),
onSuccess: (data) => {
Toaster.closeLoading('LOADING');
Toaster.openSuccess(data.message);
queryClient.invalidateQueries({ queryKey: ['MyBlog'] });
},
onError: (error) => {
Toaster.closeLoading('LOADING');
Toaster.openError(error.response?.data.message as string);
},
onMutate: (variables) => {
Toaster.openLoading('Please wait');
},
}),
},
Approve: {
useMutation: (
options?: UseMutationOptions<SuccessRes, AxiosError<ErrorRes>, string>
) =>

useMutation({
...options,
mutationFn: (id) => approveBlogFn(id),
onSuccess: (data) => {
Toaster.closeLoading('LOADING');
Toaster.openSuccess(data.message);
queryClient.invalidateQueries({ queryKey: ['MyBlog'] });
},
onError: (error) => {
Toaster.closeLoading('LOADING');
Toaster.openError(error.response?.data.message as string);
},
onMutate: (variables) => {
Toaster.openLoading('Please wait');
},
}),
},
RequestForApproval: {
useMutation: (
options?: UseMutationOptions<SuccessRes, AxiosError<ErrorRes>, string>
) =>

useMutation({
...options,
mutationFn: (id) => requestForApprovalFn(id),
onSuccess: (data) => {
Toaster.closeLoading('LOADING');
Toaster.openSuccess(data.message);
queryClient.invalidateQueries({ queryKey: ['MyBlog'] });
},
onError: (error) => {
Toaster.closeLoading('LOADING');
Toaster.openError(error.response?.data.message as string);
},
onMutate: (variables) => {
Toaster.openLoading('Please wait');
},
}),
},
UnpublishBlog: {
useMutation: (
options?: UseMutationOptions<SuccessRes, AxiosError<ErrorRes>, string>
) =>

useMutation({
...options,
mutationFn: (id) => unpublishBlogFn(id),
onSuccess: (data) => {
Toaster.closeLoading('LOADING');
Toaster.openSuccess(data.message);
queryClient.invalidateQueries({ queryKey: ['MyBlog'] });
},
onError: (error) => {
Toaster.closeLoading('LOADING');
Toaster.openError(error.response?.data.message as string);
},
onMutate: (variables) => {
Toaster.openLoading('Please wait');
},
}),
},
};

export default Blog;

Step 4. Aggregated API Request Handler

Create a central file to aggregate all feature API request handlers. This file, /util/api/index.ts, might look like this:

import Helper from './helper';
import Blog from './blog';
import Profile from './profile';
import Auth from './auth';
import Team from './team';
import Project from './project';

const api = {
Blog,
Auth,
Profile,
Helper,
Team,
Project,
};

export default api;

Step 5. Using the API Request Handler in Components

Here’s how you can use the centralized API request handler in a React component:

import apiRequest from '../path/to/apiRequest';
import { useQuery } from '@tanstack/react-query';

const BlogComponent: React.FC = () => {
const { data, error, isLoading } = apiRequest.Blog.MyBlog.useQuery();

if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;

return (
<div>
{data?.map((blog: BlogType) => (
<div key={blog._id}>
<h3>{blog.title}</h3>
<p>{blog.description}</p>
</div>
))}
</div>
);
};

export default BlogComponent;

Pros and Cons

Pros:

  • Organized and maintainable codebase.
  • Consistent error handling and state management.
  • Scales well for larger projects.

Cons:

  • Initial setup may be more complex.
  • Requires familiarity with React Query and custom hooks.

Bonus: Cheat Sheet — CRUD Generator VS Code Function

In this bonus section, I’ll guide you on how to create a VS Code snippet and set it up globally.

Step 1: Creating the VS Code Snippet

  1. Open VS Code.
  2. Go to file -> Preferences => user Snippets or configure user Snippets => New Global Snippets.
  3. Choose a file name, for example, crud-generator.json.
  4. Paste the following JSON code into the file:
{
"Create CRUD Structure": {
"prefix": "rqcrud",
"body": [
"// Make sure you have your owen Toast Component",
"import Toaster from '../../components/Toast/Toast';",
"// Make sure you error and success type defined globally",
"import { ErrorType, FileType, SuccessType } from '../../types/core';",
"import Router from 'next/router';",
"import {",
" UseMutationOptions,",
" UseQueryOptions,",
" useMutation,",
" useQuery,",
"} from '@tanstack/react-query';",
"import axios, { AxiosError } from 'axios';",
"import queryClient from '../queryClient';",
"",
"// Define your interface types here",
"interface ${TM_FILENAME_BASE/(.*)/${1:/capitalize}/}Type extends ${TM_FILENAME_BASE/(.*)/${1:/capitalize}/}FormType {",
" id: string;",
" createdAt: string;",
" // continue adding",
"}",
"",
"interface ${TM_FILENAME_BASE/(.*)/${1:/capitalize}/}FormType {",
" title: string;",
" // continue adding",
"}",
"",
"",
"// Define your CRUD API functions",
"export async function create${TM_FILENAME_BASE/(.*)/${1:/capitalize}/}Fn(data: ${TM_FILENAME_BASE/(.*)/${1:/capitalize}/}FormType) {",
" return (await axios.post('/api/${TM_FILENAME_BASE}/create', data)).data;",
"}",
"",
"export async function delete${TM_FILENAME_BASE/(.*)/${1:/capitalize}/}Fn(id: string) {",
" return (await axios.delete(`/api/${TM_FILENAME_BASE}/delete/`+id)).data;",
"}",
"",
"export async function update${TM_FILENAME_BASE/(.*)/${1:/capitalize}/}Fn(data: ${TM_FILENAME_BASE/(.*)/${1:/capitalize}/}FormType, id: string) {",
" return (await axios.put(`/api/${TM_FILENAME_BASE}/update/`+id, data)).data;",
"}",
"",
"export async function get${TM_FILENAME_BASE/(.*)/${1:/capitalize}/}Fn() {",
" return (await axios.get('/api/${TM_FILENAME_BASE}/get')).data;",
"}",
"",
"export async function get${TM_FILENAME_BASE/(.*)/${1:/capitalize}/}ByIdFn(id: string) {",
" return (await axios.get(`/api/${TM_FILENAME_BASE}/get/`+id)).data;",
"}",
"",
"",
"// Define React Query hooks for CRUD operations",
"const ${TM_FILENAME_BASE/(.*)/${1:/capitalize}/} = {",
" Create: {",
" useMutation: (options?: UseMutationOptions<SuccessType, AxiosError<ErrorType>, ${TM_FILENAME_BASE/(.*)/${1:/capitalize}/}FormType>) =>",
" useMutation({",
" ...options,",
" mutationFn: (data) => create${TM_FILENAME_BASE/(.*)/${1:/capitalize}/}Fn(data),",
" onSuccess: (data) => {",
" // Custom success handling",
" },",
" onError: (error) => {",
" // Custom error handling",
" },",
" }),",
" },",
" Delete: {",
" useMutation: (options?: UseMutationOptions<SuccessType, AxiosError<ErrorType>, string>) =>",
" useMutation({",
" ...options,",
" mutationFn: (id) => delete${TM_FILENAME_BASE/(.*)/${1:/capitalize}/}Fn(id),",
" onSuccess: (data) => {",
" // Custom success handling",
" },",
" onError: (error) => {",
" // Custom error handling",
" },",
" }),",
" },",
" Update: {",
" useMutation: (id: string, options?: UseMutationOptions<SuccessType, AxiosError<ErrorType>, ${TM_FILENAME_BASE/(.*)/${1:/capitalize}/}FormType>) =>",
" useMutation({",
" ...options,",
" mutationFn: (data) => update${TM_FILENAME_BASE/(.*)/${1:/capitalize}/}Fn(data, id),",
" onSuccess: (data) => {",
" // Custom success handling",
" },",
" onError: (error) => {",
" // Custom error handling",
" },",
" }),",
" },",
" Get: {",
" useQuery: (options?: UseQueryOptions<${TM_FILENAME_BASE/(.*)/${1:/capitalize}/}Type, AxiosError<ErrorType>>) =>",
" useQuery({",
" queryKey: ['get'],",
" queryFn: () => get${TM_FILENAME_BASE/(.*)/${1:/capitalize}/}Fn(),",
" ...options,",
" }),",
" },",
" GetById: {",
" useQuery: (id: string, options?: UseQueryOptions<${TM_FILENAME_BASE/(.*)/${1:/capitalize}/}Type, AxiosError<ErrorType>>) =>",
" useQuery({",
" queryKey: ['getById', id],",
" queryFn: () => get${TM_FILENAME_BASE/(.*)/${1:/capitalize}/}ByIdFn(id),",
" ...options,",
" }),",
" },",
"};",
"",
"export default ${TM_FILENAME_BASE/(.*)/${1:/capitalize}/};"
],
"description": "Generate comprehensive CRUD structure for create, delete, update, get, and get by id operations using react Query Axios and Typescript"
}
}

After pasting the JSON code into the file, you need to save the file. And, your VS Code snippet for the CRUD generator should be saved and ready to use. You can test it out by typing the snippet prefix (rqcrud) in a TypeScript file, then pressing Tab or Enter to trigger the snippet expansion. This will generate the Simple CRUD structure for your React application.

Conclusion

By using these methods, you can efficiently fetch data and manage state in your React applications. Whether you prefer the simplicity of the fetch API, the power of Axios, or the advanced features of React Query, there's a solution that fits your needs. For larger projects, adopting a Full structure for API requests and state management ensures a maintainable and scalable codebase.

What other techniques or tools would you add to this guide? Let me know in the comments!

Thanks for reading. If you have some time, you can also see other articles completely unrelated to this topic like

  1. Building Addictive Products by Copying TikTok and Omegle Strategy
  2. I Am a Drop Out and You might be a drop out too
  3. Documentation Driven Development — The Only Way to Code
  4. Uncover The Truth About Starting A Business (Start-Up) & Why Everyone Is Obsessed with It

or you could clap 50 times and follow me for the next post

Sign up to discover human stories that deepen your understanding of the world.

--

--

Theodore Negusu
Theodore Negusu

Written by Theodore Negusu

I sometime write and explore about things I’m passionate about. if you like Business, Design , Marketing, UI / UX or programing. subscribe!

No responses yet

Write a response