Next.js is a robust React framework. It enables developers to build fast and dynamic web applications. One key feature of Next.js is its robust data-fetching capabilities. Whether you’re creating static pages or dynamic UIs, Next.js provides flexible methods for data fetching.
In this tutorial, you’ll learn how to fetch data in both Server Components and Client Components. We’ll also cover how to stream components that rely on data.
Server-Side Data Fetching
Fetching data on the server provides better SEO and faster first loads. Next.js makes this process simple with built-in methods.
Using getServerSideProps
This method fetches data on every request. It’s ideal for pages that rely on up-to-date information.
export async function getServerSideProps(context) {
const res = await fetch('https://api.example.com/data');
const data = await res.json();
return {
props: { data }, // will be passed to the page component
};
}This function runs on the server at request time. It sends the data to your component as props.
Benefits of getServerSideProps
- Great for dynamic content.
- SEO-friendly.
- Fast performance for first loads.
However, since it runs on every request, it may increase server load.
When to Use
Use getServerSideProps When data changes frequently or requires user-specific updates.
Static Site Generation with getStaticProps
If your data changes rarely, static generation is ideal.
export async function getStaticProps() {
const res = await fetch('https://api.example.com/data');
const data = await res.json();
return {
props: { data }, // will be passed to the page component
revalidate: 60, // regenerate page every 60 seconds
};
}This function runs at build time or periodically if you use revalidate.
Benefits of getStaticProps
- Fast page loads.
- Reduced server load.
- SEO-friendly.
Use this for blog posts, documentation, or product pages.
Fetching in Server Components (Next.js 13+)
Next.js 13 introduced Server Components. These let you fetch data directly in the component file.
async function ServerComponent() {
const res = await fetch('https://api.example.com/data');
const data = await res.json();
return (
<div>
<h1>Data from Server</h1>
<p>{data.message}</p>
</div>
);
}This runs entirely on the server. It reduces the amount of TypeScript sent to the client.
Why Use Server Components
- Lower client bundle size.
- Improved performance.
- No need for useEffect or state hooks.
Server Components shine when data is not interactive or doesn’t need client-side updates.
Client-Side Fetching
Sometimes, you need to fetch data in the browser. Use React hooks like useEffect.
import { useState, useEffect } from 'react';
function ClientComponent() {
const [data, setData] = useState(null);
useEffect(() => {
async function fetchData() {
const res = await fetch('/api/data');
const json = await res.json();
setData(json);
}
fetchData();
}, []);
if (!data) return <p>Loading...</p>;
return <div>{data.message}</div>;
}This is useful for:
- User-specific dashboards.
- Live data updates.
- Interactions like search or filters.
Downsides of Client Fetching
- Not SEO-friendly.
- Slower initial load.
- Needs more JavaScript.
Use it when interactivity is key and SEO is less important.
Streaming Components with Data
Next.js 13 also supports streaming server-rendered components. It allows you to display parts of your page while waiting for data to load.
Using Suspense and async Components
Wrap slow components with Suspense.
import { Suspense } from 'react';
import SlowComponent from './SlowComponent';
export default function Page() {
return (
<div>
<h1>My Page</h1>
<Suspense fallback={<p>Loading...</p>}>
<SlowComponent />
</Suspense>
</div>
);
}In SlowComponent, fetch data using an async function:
async function SlowComponent() {
const res = await fetch('https://api.example.com/slow');
const data = await res.json();
return <p>{data.message}</p>;
}With streaming, users see content sooner. It boosts perceived performance.
Best Practices
To get the most out of Next.js data fetching:
- Select the appropriate method for your specific use case.
- Use
getStaticPropsfor rarely changing content. - Use
getServerSidePropsfor dynamic data. - Use Server Components to reduce client JS.
- Use Client Components for interactivity.
- Use Suspense to stream components for a better UX.
Optimize Performance
- Cache external API requests where possible.
- Use environment variables for secure keys.
- Paginate large datasets.
- Handle loading and error states gracefully.
Example website
It demonstrates how to fetch data in both server and client components, and how to stream with Suspense.
Project Structure
my-next-app/
├── app/
│ ├── page.tsx
│ └── slow/
│ └── page.tsx
├── components/
├── ServerFetching.tsx
└── ClientFetching.tsx
├── tailwind.config.js
├── next.config.js
├── package.json
└── postcss.config.jsSetup & Config

Home Page
app/page.tsx
import { Suspense } from "react";
import DataFetched from "../components/ServerFetching";
import BasicFetchDemo from "../components/ClientFetching";
export default function HomePage() {
return (
<div className="space-y-8">
<h1>Server-side fetching</h1>
<Suspense fallback={<p>Loading data...</p>}>
{/* Server Component: Fetches data */}
<DataFetched />
</Suspense>
<h1>Client-side fetching</h1>
<BasicFetchDemo />
</div>
);
}Server-side Components
components/ServerFetching.tsx
import Image from "next/image";
async function fetchData() {
const res = await fetch(
"https://akabab.github.io/starwars-api/api/id/4.json",
);
return res.json();
}
interface DataProps {
name: string;
image: string;
description: string;
}
function UserProfile({ name, image, description }: DataProps) {
return (
<div className="flex flex-col items-center p-6 bg-white rounded-lg shadow-md max-w-sm mx-auto my-8">
{image && (
<Image
alt={name}
className="w-32 h-32 rounded-full object-cover mb-4 border-2 border-gray-200"
height={300}
src={image}
width={300}
/>
)}
<h2 className="text-2xl font-bold text-gray-900 mb-2">{name}</h2>
<p className="text-gray-600 text-center text-sm">{description}</p>
</div>
);
}
export default async function DataFetched() {
const data = await fetchData();
return (
<UserProfile
description={`Height: ${data.height}cm • Mass: ${data.mass}kg`}
image={data.image}
name={data.name}
/>
);
}This server component fetches data at render time and displays it.
Client-Side Component
components/ClientFetching.tsx
"use client";
import { useState, useEffect } from "react";
interface Post {
userId: number;
id: number;
title: string;
body: string;
}
interface UseFetchResult<T> {
data: T | null;
loading: boolean;
error: string | null;
}
function useFetch<T>(url: string): UseFetchResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err: unknown) {
if (err instanceof Error) {
setError(err.message);
} else {
setError(String(err));
}
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
export default function BasicFetchDemo() {
const { data, loading, error } = useFetch<Post>(
"https://jsonplaceholder.typicode.com/posts/1",
);
if (loading) return <div className="p-4 text-blue-600">Loading post...</div>;
if (error) return <div className="p-4 text-red-600">Error: {error}</div>;
return (
<div className="p-4 border rounded-lg bg-gray-50">
<h3 className="font-bold text-lg mb-2">Basic Fetch Demo</h3>
<div className="space-y-2">
<h4 className="font-semibold">{data?.title}</h4>
<p className="text-gray-700">{data?.body}</p>
<p className="text-sm text-gray-500">User ID: {data?.userId}</p>
</div>
</div>
);
}Fetching data on component mount using a custom hook
Slow Page Streaming Demo
app/slow/page.tsx
import { Suspense } from "react";
async function Slow() {
await new Promise((resolve) => setTimeout(resolve, 2000));
return (
<div className="bg-white rounded-lg p-6 border border-gray-200">
<p className="text-gray-700">This loaded after a delay.</p>
</div>
);
}
export default function SlowPage() {
return (
<div className="max-w-2xl mx-auto p-8 space-y-6">
<h2 className="text-3xl font-light text-gray-900">Streaming Demo</h2>
<Suspense
fallback={
<div className="bg-gray-50 rounded-lg p-6 border border-gray-200">
<div className="flex items-center space-x-3">
<div className="w-5 h-5 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin" />
<p className="text-gray-600">Fetching slow data…</p>
</div>
</div>
}
>
<Slow />
</Suspense>
</div>
);
}This shows partial page rendering while waiting for async data.
Add images configuration
Add or modify the images property to include the domains array listing vignette.wikia.nocookie.net as an allowed domain.
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
domains: [
'vignette.wikia.nocookie.net',
],
},
};
module.exports = nextConfig;Run the App
npm run dev
http://localhost:3000

Slow-loading page demo.
http://localhost:3000/slow


Finally
Next.js gives you multiple tools for fetching data. With the latest updates, it’s easier than ever to strike a balance between speed, SEO, and interactivity.
By mastering both server and client fetching, you can build robust and responsive web apps. Now, start experimenting and choose the best strategy for your project.
This article was originally published on Medium.



