Home / blogs

React SSR with Next.js: A Step-by-Step Guide

July 11, 2024

React Server-Side Rendering (SSR) with Next.js is a powerful way to build fast, SEO-friendly, and dynamic web applications. Instead of sending an empty HTML page and rendering everything in the browser, SSR allows the server to generate HTML before sending it to the user.

In this guide, we will explore how SSR works in Next.js, how to fetch dynamic data, how to create dynamic routes, and how to optimize and deploy your application.

Why Use SSR with Next.js?

Server-Side Rendering provides several benefits for modern web applications:

  • Performance: The server can send pre-rendered HTML to the browser.
  • SEO: Search engines can access meaningful HTML content more easily.
  • Dynamic Data: Pages can display data fetched from APIs or databases.
  • Improved User Experience: Users can see useful content before all client-side JavaScript has loaded.

Understanding Server-Side Rendering

In a traditional client-rendered React application, the browser downloads JavaScript and builds the page on the client. With SSR, the server generates the initial HTML for a request.

The browser receives the HTML and can display content immediately. React then hydrates the page when client-side JavaScript is ready.

SSR is particularly useful for blogs, product pages, news websites, dashboards with server-loaded data, and other applications where initial content matters.

Project Setup

First, create a new Next.js application using the official project creation command:

npx create-next-app@latest next-ssr-app

During setup, you can select TypeScript, ESLint, Tailwind CSS, and the App Router according to your project requirements.

Move into the project directory and start the development server:

cd next-ssr-app
npm run dev

Recommended Project Structure

next-ssr-app/
├── app/
│   ├── layout.tsx
│   ├── page.tsx
│   └── posts/
│       └── [id]/
│           └── page.tsx
├── components/
├── lib/
├── public/
├── package.json
└── next.config.ts

The app directory contains routes and layouts. Thepage.tsx file defines a route's page, whilelayout.tsx defines shared layouts.

SSR in the Next.js App Router

In the App Router, pages are Server Components by default. This means you can fetch data on the server directly inside an asynchronous page component.

For example, you can fetch a list of posts from an API before the page is rendered:

// app/posts/page.tsx

async function getPosts() {
  const response = await fetch(
    "https://api.example.com/posts",
    {
      cache: "no-store",
    }
  );

  if (!response.ok) {
    throw new Error("Failed to fetch posts");
  }

  return response.json();
}

export default async function PostsPage() {
  const posts = await getPosts();

  return (
    <main>
      <h1>Latest Posts</h1>

      {posts.map((post: any) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.description}</p>
        </article>
      ))}
    </main>
  );
}

The cache: `no-store` option requests fresh data for each request. This is useful when the page needs up-to-date information.

Fetching Data on the Server

One of the main advantages of Next.js is that you can fetch data on the server without moving the entire data-fetching process into a client component.

Example: Fetching a Single Post

// app/posts/[id]/page.tsx

interface Post {
  id: number;
  title: string;
  body: string;
}

async function getPost(id: string): Promise<Post> {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/posts/${id}`,
    {
      cache: "no-store",
    }
  );

  if (!response.ok) {
    throw new Error("Post not found");
  }

  return response.json();
}

export default async function PostPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const post = await getPost(id);

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </article>
  );
}

In current Next.js App Router versions, route parameters are asynchronous in this form, so they should be awaited before use.

Dynamic Routes with SSR

Dynamic routes allow you to create pages for changing values such as blog slugs, product IDs, and user profiles.

For example, the following structure creates a dynamic blog route:

app/
└── blog/
    └── [slug]/
        └── page.tsx

A URL such as /blog/react-ssr-with-nextjs can then be handled by the dynamic page.

Reading a Dynamic Slug

export default async function BlogPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;

  return (
    <main>
      <h1>Blog: {slug}</h1>
    </main>
  );
}

SSR with getServerSideProps

If your application uses the Pages Router, Next.js providesgetServerSideProps for fetching data on every request.

This method belongs in a file inside the pages directory, not in an App Router page.

// pages/posts/[id].tsx

import type {
  GetServerSideProps,
} from "next";

interface Post {
  id: number;
  title: string;
  body: string;
}

interface Props {
  post: Post;
}

export const getServerSideProps: GetServerSideProps<Props> =
  async (context) => {
    const id = context.params?.id;

    const response = await fetch(
      `https://jsonplaceholder.typicode.com/posts/${id}`
    );

    if (!response.ok) {
      return {
        notFound: true,
      };
    }

    const post = await response.json();

    return {
      props: {
        post,
      },
    };
  };

export default function PostPage({ post }: Props) {
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </article>
  );
}

The function runs on the server for each request and passes the returned data to the page component.

Caching and Performance Optimization

Efficient caching can reduce unnecessary requests and improve application performance. The App Router supports different data fetching and caching strategies.

  • Dynamic rendering: Use fresh data when required.
  • Cached data: Reuse data when it does not change frequently.
  • Revalidation: Refresh cached content periodically.
  • Streaming: Display parts of a page as they become available.

Example: Revalidating Data

const response = await fetch(
  "https://api.example.com/posts",
  {
    next: {
      revalidate: 60,
    },
  }
);

This example requests revalidation after 60 seconds. The appropriate caching strategy depends on the data and application requirements.

Code Splitting and Client Components

Next.js automatically splits application JavaScript into smaller bundles. You can also use dynamic imports when a large component is only needed in certain situations.

import dynamic from "next/dynamic";

const Chart = dynamic(
  () => import("@/components/Chart"),
  {
    loading: () => <p>Loading chart...</p>,
  }
);

export default function Dashboard() {
  return (
    <main>
      <h1>Dashboard</h1>
      <Chart />
    </main>
  );
}

Dynamic imports can help reduce the JavaScript required for the initial page, especially when loading larger interactive components.

Optimizing Server Resources

Server performance is important when your application handles many concurrent users. Consider the following practices:

  • Use efficient database queries.
  • Cache data that does not need frequent updates.
  • Paginate large datasets.
  • Keep API responses small and relevant.
  • Monitor response times and server resource usage.
  • Use appropriate hosting and scaling strategies.

Improving SEO with Next.js

Server-rendered content can help search engines discover page content. Next.js also provides metadata APIs for titles, descriptions, and social sharing information.

// app/posts/[id]/page.tsx

import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "React SSR with Next.js",
  description:
    "Learn how to implement SSR with Next.js.",
};

export default function Page() {
  return <h1>React SSR with Next.js</h1>;
}

Use descriptive titles, relevant descriptions, accessible HTML, and meaningful content to help improve the discoverability of your pages.

Deploying a Next.js SSR Application

After completing development, you can deploy your Next.js application to a hosting platform that supports its rendering requirements.

Build the Application

npm run build
npm start

The production build checks and optimizes your application for deployment. The hosting environment should be configured with the required environment variables and backend services.

Hosting Options

  • Vercel: Provides hosting and deployment features designed for Next.js applications.
  • VPS: Gives you control over the server, reverse proxy, processes, and deployment configuration.
  • AWS: Offers several infrastructure options for applications with different hosting requirements.

Common SSR Mistakes to Avoid

  • Using getServerSideProps inside the App Router.
  • Exposing private API keys in client-side components.
  • Fetching the same data repeatedly without considering caching.
  • Making every component a Client Component unnecessarily.
  • Ignoring loading, error, and not-found states.
  • Sending large amounts of unnecessary data to the browser.

Conclusion

Next.js makes it easier to build modern React applications with server-rendered content, dynamic data, and optimized performance. Whether you use the App Router or Pages Router, understanding how server rendering works will help you create better web applications.

By combining server-side data fetching, dynamic routes, caching, optimized components, and suitable deployment strategies, developers can build scalable and SEO-friendly applications.

Learning Resources

Read More Blogs