Skip to main content

What is Next.js and why use it

Next.js is a React framework that adds server-side rendering, file-based routing, and built-in optimization so you can skip configuring all of that yourself.

Theory

TL;DR

  • React renders in the browser after JS loads; Next.js renders on the server first, sends HTML, then hydrates with React
  • File structure inside app/ becomes routes automatically, no React Router needed
  • SSR, SSG, ISR, and CSR are all available per page
  • Use Next.js for blogs, e-commerce, marketing sites where SEO matters; use plain React for dashboards and internal tools

Quick example

The difference shows up immediately in how routing works:

javascript
// Plain React - you wire up routing manually import { BrowserRouter, Routes, Route } from 'react-router-dom'; export default function App() { return ( <BrowserRouter> <Routes> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> </Routes> </BrowserRouter> ); } // Next.js - file structure is the routing // app/page.js -> / // app/about/page.js -> /about // No Router component. No configuration.

No setup. The file exists, the route exists.

Key difference

React is a UI library. It renders components in the browser after JavaScript loads, which means search engines see an empty <div> until JS executes. Next.js flips this: the server runs your component code, generates real HTML, and sends it to the browser. The user sees content immediately. Then React loads in the background and hydrates the page, attaching event listeners. Same components, same hooks, but the first paint happens on the server, not in the browser.

Rendering strategies

Next.js gives you four rendering modes, and you pick per page:

  • SSR - server renders on every request. Good for personalized content or data that changes frequently.
  • SSG - page is built once at deploy time, served from CDN. Fastest option for content that rarely changes.
  • ISR - static page that regenerates in the background on a schedule. Gets you CDN speed with fresher data.
  • CSR - regular client-side React. Use this when a page needs no SEO and is fully interactive.
javascript
// SSR: cache: 'no-store' forces a fresh server render on each request export default async function Page() { const data = await fetch('https://api.example.com/posts', { cache: 'no-store' }); const posts = await data.json(); return <PostList posts={posts} />; } // ISR: regenerates every hour, serves cached HTML in between export default async function Page() { const data = await fetch('https://api.example.com/posts', { next: { revalidate: 3600 } }); const posts = await data.json(); return <PostList posts={posts} />; }

When to use

Next.js makes sense when:

  • SEO matters: blogs, e-commerce, marketing pages
  • First load speed is a priority
  • You need server-side auth or personalization
  • You want routing, API routes, and image optimization already wired up

Plain React makes more sense when:

  • Building an admin dashboard or internal tool that Google never crawls
  • All content is generated client-side (think Figma or a real-time editor)
  • You are prototyping and want the simplest possible setup

Comparison table

AspectReact (SPA)Next.js
RoutingManual (react-router)File-based, automatic
Initial HTMLEmpty <div>Full page content
SEOPoor without extra setupGood out of the box
First loadWaits for JSHTML + CSS first
Data fetchinguseEffect on clientServer Components, fetch on server
APINoneRoute Handlers built in
DeploymentStatic hostingNode.js server or serverless
Good forDashboards, internal toolsBlogs, stores, marketing sites

How it works internally

When you request a Next.js page, the server runs your component and produces HTML. That HTML goes to the browser right away. The user sees content before any JavaScript runs. Then React loads and hydrates the page, connecting event listeners and making it interactive.

For static pages (SSG/ISR), this HTML is pre-generated and served from a CDN. The server is not involved on every request, so static pages load faster than SSR ones.

Server Components are the default in the App Router (Next.js 13+). They run only on the server, so you can read from a database directly without exposing credentials. When a component needs interactivity like useState or onClick, add 'use client' at the top of the file.

javascript
// Server Component (default) - runs on server only export default async function UsersPage() { const users = await db.user.findMany(); // direct DB access, no credentials leak return ( <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> ); } // Client Component - needs 'use client' for hooks and event handlers 'use client'; import { useState } from 'react'; export default function Counter() { const [count, setCount] = useState(0); return ( <button onClick={() => setCount(count + 1)}> Clicks: {count} </button> ); }

Common mistakes

1. Using useEffect to fetch data instead of async server components

The whole point of Server Components is that data fetching happens on the server. Reach for useEffect and you are back to client-side rendering: empty HTML ships first, data loads after JS runs.

javascript
// Wrong - defeats server rendering export default function Page() { const [data, setData] = useState(null); useEffect(() => { fetch('/api/data').then(r => r.json()).then(setData); }, []); return <div>{data?.name}</div>; // browser gets empty HTML first } // Right - fetch directly in the async component export default async function Page() { const data = await fetch('https://api.example.com/data').then(r => r.json()); return <div>{data.name}</div>; // server sends full HTML }

2. Using hooks in a Server Component

Server Components run on the server. useState, useContext, event handlers - none of these work there. The fix is 'use client' at the top of the file.

3. Hydration mismatch

If the server renders different HTML than the client, React cannot attach event listeners correctly. A common cause is rendering new Date() directly in the component.

javascript
// Wrong - server renders '10:20:18 PM', client renders '10:20:25 PM' export default function Time() { return <div>{new Date().toLocaleTimeString()}</div>; } // Right - render the time only on the client with useEffect 'use client'; import { useEffect, useState } from 'react'; export default function Time() { const [time, setTime] = useState(''); useEffect(() => setTime(new Date().toLocaleTimeString()), []); return <div>{time}</div>; }

4. Putting secret API keys in Server Component files

Server Components run on the server, but the code is included in the React Server Component payload that goes to the browser. Secret keys end up visible in network requests. Keep them in .env.local and use them only in API routes or Server Actions, not in component files.

5. Rebuilding the entire site on every deploy instead of using ISR

10,000 product pages means a full rebuild takes minutes and users see stale content during the build. Use revalidate: first request generates and caches HTML, all requests in that window serve from cache instantly, after the interval Next.js regenerates in the background while the old page still serves.

Real-world usage

  • TikTok, Hulu, Nike: marketing sites and e-commerce where SEO matters
  • Stripe, GitHub: documentation sites
  • Shopify Hydrogen: commerce framework built on Next.js
  • Notion, Linear: Next.js for public pages, plain React for internal tools
  • Vercel: their entire platform runs on Next.js, Server Components power the dashboard

Follow-up questions

Q: What is the difference between Server Components and SSR?
A: SSR is a deployment pattern where React renders HTML on a Node.js server on each request. Server Components are a React 18 feature that run on the server and stream JSX to the client. Next.js uses both: Server Components by default, SSR for dynamic pages.

Q: If Next.js renders on the server, how does it handle real-time updates?
A: Server Components render once and produce static HTML. For real-time data, use Client Components with useEffect or WebSockets. For occasional refreshes, Server Actions with revalidatePath() can trigger a fresh render for specific pages.

Q: Why would you pick SSG over SSR?
A: SSG pre-renders at build time and serves from a CDN - instant, no per-request server cost. SSR renders on every request, which is slower and more expensive. Use SSG for content that changes rarely (blog posts, product pages). Use SSR for personalized content or data that changes by the minute.

Q: What happens when a Server Component has a slow database query?
A: The page waits until the query completes before sending any HTML to the browser. Wrap slow components in <Suspense> to show a loading state while the rest of the page renders immediately. The slow component streams in once the data is ready.

Q: (Senior level) How would you structure a site where some pages are static, some are SSR, and some are CSR?
A: Use revalidate (ISR) for mostly-static pages like product listings. Use SSR (cache: 'no-store') for personalized or auth-gated content. Use Client Components for fully interactive sections. Static is fastest but can be stale. SSR is always fresh but adds server latency. CSR is interactive but loses SEO. The right split depends on how often content changes and whether search engines need to index it.

Examples

Server-side data fetching in a blog post page

javascript
// app/blog/[slug]/page.js // Runs on the server. params.slug comes from the URL segment. export default async function BlogPost({ params }) { const post = await fetch( `https://api.example.com/posts/${params.slug}` ).then(r => r.json()); // Server generates this HTML fully populated before sending to browser. // Google crawls the full post content on first request. return ( <article> <h1>{post.title}</h1> <p>{post.content}</p> </article> ); }

URL /blog/next-js-guide sets params.slug to "next-js-guide". The fetch runs on the server. The HTML that reaches the browser already has the title and content in it, not empty placeholders.

ISR for a product page with 10,000 products

javascript
// app/products/[id]/page.js export const revalidate = 3600; // regenerate every hour export default async function Product({ params }) { const product = await fetch( `https://api.example.com/products/${params.id}`, { next: { revalidate: 3600 } } ).then(r => r.json()); return ( <div> <h1>{product.name}</h1> <p>${product.price}</p> </div> ); }

First request generates and caches HTML. All requests in the next hour serve from cache instantly. After the hour, Next.js regenerates in the background while the old page still serves. Price changes appear within the hour without touching the deploy pipeline.

Mixing Server and Client Components

javascript
// app/dashboard/page.js - Server Component fetches data import StatsChart from './StatsChart'; export default async function Dashboard() { const stats = await fetch('https://api.example.com/stats', { cache: 'no-store' }).then(r => r.json()); // Pass data to a Client Component that handles interactivity return ( <main> <h1>Dashboard</h1> <StatsChart data={stats} /> </main> ); }
javascript
// app/dashboard/StatsChart.js - Client Component handles state 'use client'; import { useState } from 'react'; export default function StatsChart({ data }) { const [view, setView] = useState('weekly'); return ( <div> <button onClick={() => setView('weekly')}>Weekly</button> <button onClick={() => setView('monthly')}>Monthly</button> <p>Showing {view} data: {data[view]}</p> </div> ); }

The server does the expensive data fetch, the client handles state changes without extra network requests. I have used this split on production dashboards - the initial page load stays fast because no JS needs to run before the user sees their data.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?