Home / blogs
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.
Server-Side Rendering provides several benefits for modern web applications:
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.
First, create a new Next.js application using the official project creation command:
npx create-next-app@latest next-ssr-appDuring 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 devnext-ssr-app/
├── app/
│ ├── layout.tsx
│ ├── page.tsx
│ └── posts/
│ └── [id]/
│ └── page.tsx
├── components/
├── lib/
├── public/
├── package.json
└── next.config.tsThe app directory contains routes and layouts. Thepage.tsx file defines a route's page, whilelayout.tsx defines shared layouts.
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.
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.
// 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 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.tsxA URL such as /blog/react-ssr-with-nextjs can then be handled by the dynamic page.
export default async function BlogPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return (
<main>
<h1>Blog: {slug}</h1>
</main>
);
}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.
Efficient caching can reduce unnecessary requests and improve application performance. The App Router supports different data fetching and caching strategies.
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.
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.
Server performance is important when your application handles many concurrent users. Consider the following practices:
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.
After completing development, you can deploy your Next.js application to a hosting platform that supports its rendering requirements.
npm run build
npm startThe production build checks and optimizes your application for deployment. The hosting environment should be configured with the required environment variables and backend services.
getServerSideProps inside the App Router.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.
Related tags:

Explore the best full stack development technologies to use in 2025. Learn about modern frontend, backend, databases, APIs, and deployment tools for building fast, scalable, and reliable applications.
Read More

Learn how React Server-Side Rendering works with Next.js. Explore server-side data fetching, dynamic routes, caching, SEO optimization, and deployment strategies.
Read More

Discover the top web development stacks of 2025, their key features, and best use cases. Learn how to choose the right stack for building fast, scalable, and maintainable web applications.
Read More