Framework Guide
How to Fetch Data in Next.js
Fetching data in Next.js is simpler than in plain React — no useEffect, no loading state juggling. Here's how Server Components change everything.
Quick answer: In the App Router, mark your page async and await fetch() directly inside the component. Next.js handles the rest — no useEffect, no useState for the data.
💡 The Big Idea
In plain React, you fetch data inside useEffect, manage loading state, handle errors manually. It's noisy.
In Next.js App Router, components can be async functions. They run on the server, fetch data, and return JSX. No client-side state needed.
Fetch in a Server Component
Open app/page.tsx (or create a new page) and write:
export default async function Home() {
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
return (
<main className="p-10">
<h1 className="text-3xl font-bold mb-6">Latest Posts</h1>
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</main>
);
}
That's it. No useState, no useEffect. The fetch happens on the server before the page is sent to the browser.
💡 Server Components are the default in Next.js. You only add "use client" at the top of a file when you need interactivity (buttons, state, browser APIs).
Add a Loading State
Next.js shows a loading UI automatically if you add a loading.tsx file next to your page:
// app/loading.tsx
export default function Loading() {
return (
<div className="p-10 text-slate-500">
Loading...
</div>
);
}
Now while the fetch is happening, visitors see "Loading..." — no extra code needed on the page itself.
Handle Errors
Add an error.tsx file to catch failures gracefully:
// app/error.tsx
'use client';
export default function Error({ error, reset }) {
return (
<div className="p-10 text-center">
<h2 className="text-xl font-bold mb-4">
Something went wrong.
</h2>
<p className="text-slate-500 mb-4">{error.message}</p>
<button
onClick={() => reset()}
className="bg-blue-500 text-white px-4 py-2 rounded"
>
Try again
</button>
</div>
);
}
Note the "use client" — error boundaries must be client components.
Fetch in a Client Component
When you need interactive fetching — like fetching on button click — use a client component:
'use client';
import { useState } from 'react';
export default function Search() {
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
async function handleSearch(query) {
setLoading(true);
const res = await fetch(`/api/search?q=${query}`);
const data = await res.json();
setResults(data);
setLoading(false);
}
return (
<div>
<button onClick={() => handleSearch('react')}>
Search React
</button>
{loading && <p>Loading...</p>}
<ul>
{results.map(r => <li key={r.id}>{r.title}</li>)}
</ul>
</div>
);
}
Use client components only when necessary — Server Components are faster and better for SEO.
🧠 Caching and Revalidation
Next.js caches fetches by default. Control it with options:
// Cache forever (default in some cases)
fetch(url, { cache: 'force-cache' });
// Never cache — fetch fresh every time
fetch(url, { cache: 'no-store' });
// Revalidate every 60 seconds
fetch(url, { next: { revalidate: 60 } });
If your data changes often, use no-store. If it rarely changes, use revalidate to refresh on a schedule.
🚫 Common Beginner Mistakes
- Adding "use client" everywhere. That defeats the purpose. Only client components when you need interactivity.
- Using useEffect in Server Components. useEffect doesn't run on the server. Fetch directly instead.
- Forgetting await.
fetch()returns a promise — you must await it, then await.json(). - Trying to use browser APIs on the server.
window,localStorage, anddocumentdon't exist on the server. - Not handling errors. Add an
error.tsx— otherwise users see a broken page.
🎯 Practice Challenges
- Fetch a list of users from
jsonplaceholder.typicode.com/users;
- Add a
loading.tsxthat shows skeleton cards - Add an
error.tsxthat has a retry button - Build a search box in a Client Component that fetches on submit
- Revalidate data every 30 seconds with
next: { revalidate: 30 }