A Complete Guide to Fetching Data in Next.js with Axios and React-TanStack-Query
Alright, let’s have an honest conversation. You’ve been using the good old fetch API with useState and useEffect. It works, right? You fetch data, store it in state, and display it. But let’s be real—it gets messy fast. Error handling? Tedious. Caching? A nightmare. What about refetching data when something changes? Forget about it! So, what if I told you there’s a better way? A way that feels like upgrading from juggling balls to using a conveyor belt. Enter Axios and React-TanStack-Query—tools that make fetching data feel like magic. Imagine you’re building an app that shows a list of movies. Instead of writing repetitive boilerplate code, these tools let you focus on building features. Ready to upgrade? Let’s dive in! Why Not Fetch + useState + useEffect? Before we jump into the new stuff, let’s quickly reflect on why we’re here: Repetition: Every time you fetch data, you repeat the same pattern—loading state, error handling, and the fetch call itself. Caching: Fetch doesn’t remember the data you’ve already fetched. If you navigate back to the same page, it fetches everything again. Refetching: What if the data changes? With fetch, you’d have to manually trigger a reload. Sound familiar? Let’s fix it. Step 1: Install Axios and React-TanStack-Query Start by adding these tools to your project: npm install axios @tanstack/react-query We’ll also set up a Query Client, which is like a helper to manage your data. // /components/providers/QueryProvider.jsx "use client" import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; const queryClient = new QueryClient(); export default function QueryProvider({ children}) { return ( {children} ); } // /layout.jsx import localFont from "next/font/local"; import "./globals.css"; import QueryProvider from "../components/providers/QueryProvider" const geistSans = localFont({ src: "./fonts/GeistVF.woff", variable: "--font-geist-sans", weight: "100 900", }); const geistMono = localFont({ src: "./fonts/GeistMonoVF.woff", variable: "--font-geist-mono", weight: "100 900", }); export const metadata= { title: "Tanstack Query with axios", description: "Generated by create next app", }; export default function RootLayout({ children, }) { return ( {children} ); } That’s it for setup. Now let’s fetch some data! Fetching Data the React-TanStack-Query Way Let’s rewrite a simple fetch example using React-TanStack-Query. Imagine we’re building a movie app and need to fetch a list of movies: Using Fetch + useState + useEffect (The Old Way) import { useEffect, useState } from "react"; export default function Movies() { const [movies, setMovies] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { fetch("https://api.example.com/movies") .then(res => res.json()) .then(data => { setMovies(data); setLoading(false); }) .catch(err => { setError(err); setLoading(false); }); }, []); if (loading) return Loading movies...; if (error) return Error loading movies: {error.message}; return ( {movies.map(movie => ( {movie.title} ))} ); } Now, let’s simplify this with React-TanStack-Query. Using React-TanStack-Query (The Better Way) import { useQuery } from "@tanstack/react-query"; import axios from "axios"; const fetchMovies = async () => { const response = await axios.get("https://api.example.com/movies"); return response.data; }; export default function Movies() { const { data: movies, error, isLoading } = useQuery(["movies"], fetchMovies); if (isLoading) return Loading movies...; if (error) return Error loading movies: {error.message}; return ( {movies.map(movie => ( {movie.title} ))} ); } What Just Happened? useQuery: This magical hook takes care of fetching, caching, and error handling. No more useState or useEffect juggling! Automatic Refetching: If something changes, your data stays up-to-date without you lifting a finger. Axios: We use Axios to fetch data, which is easier to work with than the fetch API. Customizing Axios for Your App In a real app, you might need to add headers, a base URL, or an authentication token to your requests. Here’s how you can create a reusable Axios instance: // utils/axios.js import axios from "axios"; const axiosInstance = axios.create({ baseURL: "https://api.example.com", headers: { Authorization: `Bearer ${process.env.API_TOKEN}`, }, }); export default axiosInstance; Now use this instance in your query: import { useQuery } from "@tanstack/react-query"; import axiosInstance from "../utils/a
Alright, let’s have an honest conversation. You’ve been using the good old fetch
API with useState
and useEffect
. It works, right? You fetch data, store it in state, and display it. But let’s be real—it gets messy fast. Error handling? Tedious. Caching? A nightmare. What about refetching data when something changes? Forget about it!
So, what if I told you there’s a better way? A way that feels like upgrading from juggling balls to using a conveyor belt. Enter Axios and React-TanStack-Query—tools that make fetching data feel like magic.
Imagine you’re building an app that shows a list of movies. Instead of writing repetitive boilerplate code, these tools let you focus on building features. Ready to upgrade? Let’s dive in!
Why Not Fetch + useState + useEffect?
Before we jump into the new stuff, let’s quickly reflect on why we’re here:
- Repetition: Every time you fetch data, you repeat the same pattern—loading state, error handling, and the fetch call itself.
- Caching: Fetch doesn’t remember the data you’ve already fetched. If you navigate back to the same page, it fetches everything again.
-
Refetching: What if the data changes? With
fetch
, you’d have to manually trigger a reload.
Sound familiar? Let’s fix it.
Step 1: Install Axios and React-TanStack-Query
Start by adding these tools to your project:
npm install axios @tanstack/react-query
We’ll also set up a Query Client, which is like a helper to manage your data.
// /components/providers/QueryProvider.jsx
"use client"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
export default function QueryProvider({ children}) {
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
}
// /layout.jsx
import localFont from "next/font/local";
import "./globals.css";
import QueryProvider from "../components/providers/QueryProvider"
const geistSans = localFont({
src: "./fonts/GeistVF.woff",
variable: "--font-geist-sans",
weight: "100 900",
});
const geistMono = localFont({
src: "./fonts/GeistMonoVF.woff",
variable: "--font-geist-mono",
weight: "100 900",
});
export const metadata= {
title: "Tanstack Query with axios",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}) {
return (
<html lang='en'>
<body
className={``}
>
<QueryProvider>{children}</QueryProvider>
</body>
</html>
);
}
That’s it for setup. Now let’s fetch some data!
Fetching Data the React-TanStack-Query Way
Let’s rewrite a simple fetch
example using React-TanStack-Query. Imagine we’re building a movie app and need to fetch a list of movies:
Using Fetch + useState + useEffect (The Old Way)
import { useEffect, useState } from "react";
export default function Movies() {
const [movies, setMovies] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch("https://api.example.com/movies")
.then(res => res.json())
.then(data => {
setMovies(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, []);
if (loading) return <p>Loading movies...</p>;
if (error) return <p>Error loading movies: {error.message}</p>;
return (
<ul>
{movies.map(movie => (
<li key={movie.id}>{movie.title}</li>
))}
</ul>
);
}
Now, let’s simplify this with React-TanStack-Query.
Using React-TanStack-Query (The Better Way)
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
const fetchMovies = async () => {
const response = await axios.get("https://api.example.com/movies");
return response.data;
};
export default function Movies() {
const { data: movies, error, isLoading } = useQuery(["movies"], fetchMovies);
if (isLoading) return <p>Loading movies...</p>;
if (error) return <p>Error loading movies: {error.message}</p>;
return (
<ul>
{movies.map(movie => (
<li key={movie.id}>{movie.title}</li>
))}
</ul>
);
}
What Just Happened?
-
useQuery
: This magical hook takes care of fetching, caching, and error handling. No moreuseState
oruseEffect
juggling! - Automatic Refetching: If something changes, your data stays up-to-date without you lifting a finger.
-
Axios: We use Axios to fetch data, which is easier to work with than the
fetch
API.
Customizing Axios for Your App
In a real app, you might need to add headers, a base URL, or an authentication token to your requests. Here’s how you can create a reusable Axios instance:
// utils/axios.js
import axios from "axios";
const axiosInstance = axios.create({
baseURL: "https://api.example.com",
headers: {
Authorization: `Bearer ${process.env.API_TOKEN}`,
},
});
export default axiosInstance;
Now use this instance in your query:
import { useQuery } from "@tanstack/react-query";
import axiosInstance from "../utils/axios";
const fetchMovies = async () => {
const response = await axiosInstance.get("/movies");
return response.data;
};
export default function Movies() {
const { data: movies, error, isLoading } = useQuery(["movies"], fetchMovies);
if (isLoading) return <p>Loading movies...</p>;
if (error) return <p>Error loading movies: {error.message}</p>;
return (
<ul>
{movies.map(movie => (
<li key={movie.id}>{movie.title}</li>
))}
</ul>
);
}
Why Use React-TanStack-Query?
Here’s why it’s worth ditching fetch
for TanStack Query:
- Caching: Your data is cached, so if you revisit the page, it doesn’t refetch unless necessary.
-
Error Handling: No need for messy
try/catch
blocks—it’s built-in. - Stale-While-Revalidate: TanStack Query shows cached data immediately while it fetches fresh data in the background.
- Flexibility: You can easily customize fetching, polling, retries, and more.
Bonus: Pagination Example
What if you’re fetching a paginated API? TanStack Query has you covered:
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
import { useState } from "react";
const fetchMovies = async (page) => {
const response = await axios.get(`https://api.example.com/movies?page=${page}`);
return response.data;
};
export default function PaginatedMovies() {
const [page, setPage] = useState(1);
const { data, error, isLoading } = useQuery(["movies", page], () => fetchMovies(page));
if (isLoading) return <p>Loading movies...</p>;
if (error) return <p>Error loading movies: {error.message}</p>;
return (
<div>
<ul>
{data.movies.map(movie => (
<li key={movie.id}>{movie.title}</li>
))}
</ul>
<button onClick={() => setPage((prev) => Math.max(prev - 1, 1))}>Previous</button>
<button onClick={() => setPage((prev) => prev + 1)}>Next</button>
</div>
);
}
Wrapping It Up
Switching to React-TanStack-Query feels like upgrading from a bicycle to a Tesla. It takes care of caching, error handling, and refetching for you, so you can focus on building cool features.
If you’re tired of boilerplate code, give it a shot. You’ll thank yourself later!