Next.js Tutorial: Fetching Data Made Easy

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 getStaticProps for rarely changing content.
  • Use getServerSideProps for 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.js

Setup & 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
Home page

Slow-loading page demo.

http://localhost:3000/slow
Loading
Loaded

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.

Leave a Comment

Your email address will not be published. Required fields are marked *