The best way to fetch data in React and Next.js

Haseeb
5 min readNov 6, 2023

--

In the world of web development, making HTTP requests is a fundamental task. Developers often face the choice of which library or tool to use for this purpose. In this article, we will compare three popular options for handling HTTP requests in TypeScript: fetch, axios, and tanstack react-query

fetch
Fetch is a built-in JavaScript method for making HTTP requests. It provides a simple and native way to send and receive data from a server. Here’s how you can make a GET request using Fetch in TypeScript

fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));

fetch is built-in so it is lightweight but did not handle properly any caching and error state. and when you are making an post you have to pass header and content types etc check following example

const url = 'https://api.example.com/postData';

const data = {
key1: 'value1',
key2: 'value2',
};

const requestOptions = {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
},
};

fetch(url, requestOptions)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error('Error:', error));

it has very ugly syntax but axios has much cleaner syntax then fetch lets explore it also together.

axios

Axios is a popular JavaScript library for making HTTP requests. It simplifies the process of making requests and provides additional features, such as request and response interceptors. Here’s how you can use axios to make the same GET request

First install it

npm install axios
import axios from 'axios';

axios.get('https://api.example.com/data')
.then(response => console.log(response.data))
.catch(error => console.error(error));

to making post request

import axios from 'axios';

const url = 'https://api.example.com/postData';

const headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
};

const data = {
key1: 'value1',
key2: 'value2',
};

axios.post(url, data, { headers })
.then(response => console.log(response.data))
.catch(error => console.error('Error:', error));

you guys can clearly see that axios has much cleaner syntax then fetch and also efficient then fetch but it cannot manage any error and loading state we manually have to manage loading and error states. But do not worry here is today our topic hero comes @tanstack-react-query .

React Query

React-Query is a powerful library designed specifically for handling data fetching and state management , handling errors ,loadings, caching and recently they release a banger in 5.0 optimistic updates. It abstracts the complexities of data management, offering a highly efficient and developer-friendly experience. Here’s an example of using React-Query to fetch data in a React

first install it and then use it

npm install @tanstack/react-query
import { useQuery } from '@tanstack/react-query';

const MyComponent = () => {
const { data, error, loading } = useQuery('data', () =>
fetch('https://api.example.com/data').then(response => response.json())
);

if (error) {
return <div>Error: {error.message}</div>;
}
return
<>
{loading?<Text>Loading...</Text>:<div>Data: {data}</div>}
</>
};

for post or any mutation request

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

const PostData = () => {
// Define a mutation function
const postMutation = useMutation((newData) =>
axios.post("url")
}).then((response) => response.json())
);

const handlePost = async () => {
// Replace with your data
const newData = {
key1: 'value1',
key2: 'value2',
};

// Execute the mutation
await postMutation.mutateAsync(newData);
};

return (
<div>
<button onClick={handlePost} disabled={postMutation.isLoading}>
{postMutation.isLoading ? 'Posting...' : 'Post Data'}
</button>

{postMutation.isError && (
<div>An error occurred: {postMutation.error.message}</div>
)}

{postMutation.isSuccess && (
<div>Data Posted Successfully: {postMutation.data}</div>
)}
</div>
);
};

you can clearly see that with react query how in beautiful way you can handle different loading and error state for further knowledge visit their official docs

Optimistic Updates

When you optimistically update your state before performing a mutation, there is a chance that the mutation will fail. In most of these failure cases, you can just trigger a refetch for your optimistic queries to revert them to their true server state. In some circumstances though, refetching may not work correctly and the mutation error could represent some type of server issue that won’t make it possible to refetch. In this event, you can instead choose to rollback your update.

see example given below if user click on add button it immediately add item and then check is promise get resolve then keep adding it if promise get rejected then it will automatically disappear after response will comeback. this powerful feature give better user experience.

import { useMutation,useQuery } from "@tanstack/react-query";

let index =3;
type Todo ={
id:number;
task:string;
}
const todos :Todo[]= [
{
id: 1,
task: 'Buy apples'
},
{
id: 2,
task: 'Buy bananas'
},
{
id: 3,
task: 'Buy oranges'
},
{
id: 4,
task: 'Buy strawberries'
}
];

export default function Home() {
const {mutate,isPending,variables}= useMutation({
mutationFn:async (newTodo:Todo)=>{
await new Promise((resolve)=>setTimeout(resolve,5000));
todos.push(newTodo);
index++;
},

})

const {data} =useQuery({
queryKey:['todos'],
queryFn:()=>todos
})

return (

<div className="flex items-start justify-start min-h-screen
py-2 mt-20 px-20 gap-8">

<button onClick={()=>
mutate({id:index,task:`watermalon${index}`})}
className="bg-purple-600 px-6 py-3 text-white">
Add
</button>
<ul >
{data?.map((todo)=>(
<li key={todo.id}>{todo.task}</li>

))}
{isPending && <li className="opacity-50 text-white">
{variables.task}</li>}
</ul>

</div>
)

}

infinite scrolling with pagination

import { useInfiniteQuery } from "@tanstack/react-query";
import { useIntersection } from "@mantine/hooks";
interface Post {
userId: number;
id: number;
title: string;
body: string | null | undefined;
}
let posts: Post[] = [];

const fetchPost = async (page: number) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return posts.slice((page - 1) * 2, page * 2);
};

export default function Home() {
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/posts")
.then((response) => response.json())
.then((data) => {
// Destructure the data into the 'posts' array
posts = [...data];

// console.log(posts);
})
.catch((error) => {
console.error("Error:", error);
});
}, []);
const { data, fetchNextPage, isFetchingNextPage } = useInfiniteQuery(
["query"],
async ({ pageParam = 1 }) => {
const res = await fetchPost(pageParam);
return res;
},
{
getNextPageParam: (_, pages) => {
return pages.length + 1;
},
initialData: {
pages: [posts.slice(0, 2)],
pageParams: [1],
},
}
);

const lastPostRef = useRef<HTMLElement>(null);

const { ref, entry } = useIntersection({
root: lastPostRef.current,
threshold: 1,
});
useEffect(() => {
if (entry?.isIntersecting) fetchNextPage();
}, [entry]);

const _posts = data?.pages.flatMap((page) => page);

return (
<div>
post:
{_posts?.map((post, i) => {
if (i === _posts.length - 1)
return (
<>
<div
className="h-80 bg-white text-3xl text-black"
ref={ref}
key={post.id}
>
{post.title}
</div>
<p>{post.body}</p>
</>
);

return (
<>
<div className="h-80 bg-white text-3xl text-black" key={post.id}>
{post.title}
</div>
<p>{post.body}</p>
</>
);
})}
<button onClick={() => fetchNextPage()}
disabled={isFetchingNextPage}>
{isFetchingNextPage
? "loading more..."
: (data?.pages.length ?? 0) < 3
? "Load more"
: "nothing more"}
</button>
</div>
);
}

above is a example of infinite scrolling and pagination with react query i will make soon a complete blog on it so stay with me.

--

--