Next.js - The React Framework
Next.js enables you to create full-stack Web applications by extending the latest React features, and integrating powerful Rust-based JavaScript tooling for the fastest builds.
Next.js is a powerful and versatile framework for building modern web applications with JavaScript and React. Developed by Vercel, Next.js has quickly become one of the most popular tools in the world of web development. What sets Next.js apart is its unique approach to building web applications. It seamlessly combines the best of server-side rendering and client-side rendering, offering an exceptional developer experience while optimizing for performance.
React vs Next.js
React is a JavaScript library for building user interfaces. It focuses on the "view" part of the application and provides a component-based architecture for creating dynamic and interactive web applications.
Next.js is a framework built on top of React. It extends React's capabilities to enable server-side rendering, routing, and other features, making it suitable for building fullstack web applications.
The key difference between Next.js and React is that with Next.js, we can create not just a pure frontend but the whole web application, including the server side. Next.js can be called a framework for developing full-stack web applications. For frontend development, Next.js uses all the power of React, building on its latest technologies and extending its capabilities.
The primary differences between React and Next.js include:
- Server-side rendering (SSR) - Next.js allows you to render components on the server, which improves performance and SEO.
- Static site generation (SSG) - Next.js allows for static site generation without rendering on the client, also improving performance and SEO, not just static web apps.
- Routing - Next.js has a built-in file-based routing system, making creating multi-page applications and organizing code easier.
- Data fetching - Next.js allows you to access your database or other data directly in components without interacting with the API. Alternatively, Next.js allows fetching data from the API on the server, not just the client.
Other benefits of Next.js include:
- Metadata API - Next.js makes setting up metadata for each page easy, again improving SEO.
- Incremental static regeneration (ISR) - Next.js allows you to set up scheduled or immediate static page regeneration, useful for pages that change infrequently.
- Caching - Next.js can cache the result of a data fetch, a component or a whole page with the
'use cache'directive, and invalidate it on demand. - Layouts - Next.js has introduced a new page structure model where individual nested pages can contain their own
layoutcomponents that nest into each other incrementally. - ...and more
SSR & SEO
React primarily renders components on the client-side. React Server Components (RSC) were unveiled by the React team in December 2020. However, employing them with the vanilla React library isn't straightforward, as it necessitates supplementary configurations and setup. Since React is still mostly client-side, achieving robust SEO performance remains an issue. This is because an empty HTML page is transmitted to the client initially, and it's the responsibility of JavaScript to render the content. You can dive deeper into the topic or RSC in this article or in this video.
Next.js takes advantage of RSCs and enables server-side rendering. In fact, all components are server components by default. This is fundamentally different from React, as Next.js dispatches pre-rendered HTML, which is already populated with the correct data (fetched from the server, an API, a database, or any other source). Alternatively, you can decide as a developer that a long-running operation will happen in the background and the component will be streamed in once finished, while a loading component is displayed immediately. This approach empowers the potential for achieving excellent SEO performance. Moreover, Next.js offers a metadata API that allows for configuring page-specific elements like titles, descriptions, and tags, further enhancing SEO capabilities.
Routing
To simulate routes in an SPA React application, you need to use 3rd party libraries like React Router or TanStack Router. Next.js, on the other hand, comes with a built-in routing system that allows you to create multi-page applications with ease. It also supports dynamic routes, which are useful for creating pages that share the same layout but have different content.
Data fetching
In React, data retrieval usually occurs on the client side, employing tools such as axios or the fetch API. In Next.js, data can be fetched from both the client and the server. With React Server Components (RSC), you gain direct access to your resources, such as databases, directly within your component. This eliminates the need to wait for data to arrive from the server before rendering your page; instead, you have immediate access to the required data right within your component.
Different ways to render a page
We already know how a pure React application works. In Next.js, you have the freedom to choose a page rendering strategy per page.
Static site generation (SSG) - the whole page is rendered at build time. By default, Next.js will try to pre-render as much of every page as it can at build time.
Server-side rendering (SSR) - used to require the use of the getServerSideProps function for an entire page in Next.js, where all data had to be fetched on the server and then sent to the client. However, this approach had its drawbacks in terms of developer experience, as data might be required deep within the component tree, necessitating its passing down to the component. Moreover, if a server request took a significant amount of time, it could block the entire page from rendering, even if only a single component on the page needed that particular data. Starting from Next.js v13, React Server Components (RSC) provide a more granular solution, allowing data to be fetched on the component level, addressing these limitations.
Incremental static regeneration (ISR) - pages created using SSG are static, so their content doesn't change until you rebuild the whole application. But sometimes, you want the information on those pages to update regularly or when something specific happens. With ISR, you can set a schedule for when these updates should happen, or you can make them happen whenever you want. A good example is a blog page, where most of the content doesn't change often, but you might want to add new blog posts now and then.
Partial prerendering (PPR) - since Next.js v16, the other strategies are no longer chosen for a whole page at once. With the cacheComponents: true option in next.config.ts (not true by default), Next.js prerenders a static shell of every route at build time and streams the request-specific parts in at request time. Everything that is static, or explicitly cached with the 'use cache' directive, becomes part of the shell; everything that reads request data (cookies, headers, search params) or fetches uncached data must sit behind a <Suspense> boundary and streams in later. We will go through this model in detail in the following lectures.
Routing fundamentals
The skeleton of every application is routing. Unlike React, Next.js has built-in file-based routing. With Next.js v13, it became possible to define a common layout for route segments, dynamic routes etc.
In this tutorial, we have not yet discussed any routing method for our application. We can use several libraries created for React to solve routing in an SPA. Since Next.js has its own routing system and it doesn't make sense to use any other approach, we haven't talked about any other libraries during the semester, and now we'll focus directly on the built-in solution of Next.js.
Terminology
URL segment - part of the URL path delimited by slashes, URL path - Whole path that comes after the domain (composed segments)
Pages vs App directory
Before Next.js v13, the only way to define routes was to use pages directory, where with Next.js v13, new app directory was introduced. In this course, you should use the new app directory in most cases. However, it is still possible to use pages directory alongside with app directory, mainly due to backward compatibility.
Roles of folders and files
Next.js has router based on folder structure. Each folder in a route represents a route segment. Each route segment is mapped to a corresponding segment in a URL path.
In order to create nested route such as /dashboard/settings, you need to nest folders inside each other.
File conventions
Next.js provides a set of special files to create UI with specific behavior in nested routes:
- layout - defines shared UI for a segment and its children. Every Next.js application has a root layout, which must include the
<html>and<body>tags. Here you can, for example, define your context providers or theQueryClientProviderfor React Query. - page - defines unique UI for a route. For example, if you want to have a
/dashboardroute, you first need to create a folder nameddashboardand inside this folder createpage.tsxto define the UI for this route. Optionally, you can also have alayout.tsxfile inside this folder to wrap the UI defined inpage.tsxwith some shared UI. - loading - your page is by default a server component that can optionally fetch some data, which can obviously take some time. In order to provide an instant page for the user, you can optionally define a
loading.tsxfile, which will be displayed immediately while your page is fetching data. Under the hood it is nothing more than a<Suspense>boundary wrapped around the page. - not-found - if you want to define a custom 404 page, you can create a
not-found.tsxfile, which will be displayed when the user navigates to a non-existing route or when you callnotFound(). - error - if you want to define a custom error page, you can create an
error.tsxfile, which will be displayed when an error occurs during page rendering. Aglobal-error.tsxfile in the root of theappdirectory catches errors thrown by the root layout itself. - template - similar to layout, but it does not persist state between route changes.
- route - inside
app/api, you can define folders which can then containroute.tsfiles that define a server side API endpoint. For example, if you want to have a GET endpoint/api/users, you need to create a folderusersinsideapp/apiand inside this folder create aroute.tsfile, which will export a function namedGET. - default - a fallback UI for a parallel route slot. Since Next.js v16, every parallel route slot must have its
default.tsxfile, otherwise the build fails. - forbidden / unauthorized - UI rendered when you call the
forbidden()orunauthorized()function (403 and 401 responses).
Two more conventions live next to the app directory rather than inside it:
- proxy -
proxy.ts(in the project root or insrc) runs before a request is matched against a route and can rewrite or redirect it. Until Next.js v16 this file was calledmiddleware.tsand the exported function was calledmiddleware. The name was changed because "middleware" was too easily confused with Express-style middleware, and because this file describes the network boundary of the app.middleware.tsstill works, but it is deprecated. Note thatproxyalways runs on the Node.js runtime; the edge runtime is not supported there. - instrumentation -
instrumentation.tsis used to register observability/monitoring tooling when the server starts.
Component Hierarchy
As in React, the components form a tree hierarchy. The basic hierarchy of a single page in Next.js looks like this:
If we look at the page from the inside out, first, there is an error boundary in the tree that renders the
NotFoundcomponent if the route the user accessed does not exist. If it exists and the component is an asynchronous RSC, theLoadingcomponent is displayed if specified. The' Error' component is displayed if there is an error on the page. The most external component is then theLayoutcomponent, which wraps the entire page. This means that if the page loads, theLayoutis already rendered.
In the case of the
/dashboard/settingsnested route, note that if/dashboard/settingsdoes not have itsLoadingcomponent defined, but/dashboarddoes, this loading will be used when joining/dashboard/settings. At the same time, note the hierarchy, where theLayoutcomponents of both routes nest within each other.
Colocation
Not only these specific files can occur inside app directory. You can create other files, that can be components, styles, utils, ... and colocate them with specific route. Routing system will ignore these files as long as they're not named with one of the special names. It might make sense, if you have a component, that is really specific for one route and you don't want to use it anywhere else, to not put it into shared components folder, but rather colocate it with specific route.
This is different from the pages directory, where any file in
pagesis considered a route.
Project organization
Apart from routing folder and file conventions, Next.js is unopinionated about how you organize and colocate your project files.
Private folders - inside the app directory, if you define a folder starting with _, it will be ignored by the routing system (for example _components).
Route groups
To organize routes without affecting the URL, create a group to keep related routes together. The folders in parenthesis will be omitted from the URL (e.g. (marketing) or (shop)).
-
You can create a folder using parenthesis
(folderName), which indicates that this folder is for organizational purposes and should not be considered a route segment. -
Route groups are not used only for colocation purposes. You can also use them to define shared layout for multiple routes.
In this example, we have
/dashboard,/dashboard/overview/dashboard/settingsroutes, all three routes share the same layout, but/dashboardand/dashboard/overviewhave another shared layout, which is not applied for/dashboard/settings.
- Multiple root layouts - with the same approach as above, you can create multiple root layouts. Normally, when you route between pages, your app is not fully reloaded; Next.js uses something called soft navigation. However, if you use more root layouts, Next.js will need to reload the whole application, since each root layout defines its own
<html>tag.
Pages and layouts
You can define a new page by creating a page.tsx file inside some folder in the app directory. All components (including page components) are server components by default. You can turn them into client components using the "use client" directive at the very top of the file. As a server component, a page can fetch data by simply making the component async and awaiting some promise inside the component.
'use client';
export default function ClientPage() {
const { data } = useQuery(/* ... */);
return <h1>Hello, {data}</h1>;
}
export default async function Page() {
const response = await fetch('https://example.com/data');
const text = await response.text();
return <h1>Hello, {text}</h1>;
}
Layouts
A layout is UI that is shared between multiple pages. On navigation, layouts preserve state and do not re-render. Layouts can also be nested. A layout always accepts a children prop, which is the page or the nested layout. A layout can be either a server or a client component. The root layout of a Next.js application is required and must be a server component. Optionally, route segments or route groups can have their own layout as well.
Layouts can also fetch data if they are server components (obviously, you can also perform client side fetching using the useQuery hook if you mark the layout as a client component). It is not possible to pass data from a layout to a page (or to children in general). However, you can use the same fetch function in both the layout and the page or any other child component, and that fetch will be executed just once (more on this later).
import type { PropsWithChildren } from 'react';
export default function DashboardLayout({
children // will be a page or nested layout
}: PropsWithChildren) {
return (
<section>
{/* Include shared UI here e.g. a header or sidebar */}
<nav>...</nav>
{children}
</section>
);
}
Since Next.js v15.5, the framework generates a globally available LayoutProps type for every route, so you don't have to type children, params and parallel route slots by hand. The types are generated by next dev, next build or next typegen and do not need to be imported.
export default function DashboardLayout(props: LayoutProps<'/dashboard'>) {
return (
<section>
<nav>...</nav>
{props.children}
</section>
);
}
Linking and Navigation
By default, the browser performs a hard navigation between pages. This means the browser reloads the page and resets React state such as useState hooks in your app and browser state such as the user's scroll position or focused element. However, in Next.js, the App Router uses soft navigation. This means React only renders the segments that have changed while preserving React and browser state, and there is no full page reload.
There are two ways to navigate between routes in Next.js:
- Using the
Linkcomponent - Using the
useRouterhook
Link component
It's a built-in component that extends the HTML <a> tag to provide prefetching and client-side navigation between routes. It is the preferred way to navigate between routes in Next.js.
import Link from 'next/link';
export default function Page() {
return <Link href="/dashboard">Dashboard</Link>;
}
You might need to check which page the user is currently on and mark the corresponding Link as active. You can do that by using the usePathname hook. As it is a hook, it can only be used inside a client component.
'use client';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
export function Navigation() {
const pathname = usePathname();
return (
<nav>
<ul>
<li>
<Link className={`${pathname === '/' ? 'active' : ''}`} href="/">
Home
</Link>
</li>
<li>
<Link
className={`${pathname === '/about' ? 'active' : ''}`}
href="/about"
>
About
</Link>
</li>
</ul>
</nav>
);
}
There are other optional props you can pass to <Link>. See the API reference for more.
useRouter hook
This hook proves useful in various scenarios. You can use useRouter to programmatically navigate to different pages in response to user actions or other events. For example, when a user clicks a button and you want to take them to a new page after a certain action is completed.
'use client';
import { useRouter } from 'next/navigation';
export default function Page() {
const router = useRouter();
return (
<button type="button" onClick={() => router.push('/dashboard')}>
Dashboard
</button>
);
}
redirect
You can use the redirect function to redirect the user to another page. It is useful, for example, when the user is not authenticated and you want to redirect them to the login page. This function is used in server components, route handlers and server actions. The component does not need to return any JSX if it is redirecting - the render is terminated at that point.
export default async function Page() {
const user = await getUser();
if (!user) {
redirect('/login');
}
return (
<div>
<h1>Welcome, {user.name}</h1>
</div>
);
}
How navigation and routing work
Prefetching
Prefetching involves preloading a route in advance, behind the scenes, before the user actually accesses it. This offers the advantage that when a user clicks on a link, the page loads instantly because it was already prefetched. If the page is a server component that involves data fetching, the data is already retrieved in the background, making it instantly accessible when the user clicks the link.
Both the Link component and the useRouter hook allow you to prefetch a route. A Link is prefetched once the page containing it is loaded and the link is visible in the viewport. With the useRouter hook, you can prefetch a route using the router.prefetch method.
Next.js prefetches every Link by default, but only in production - prefetching is disabled in next dev. You can turn it off by passing the prefetch={false} prop to the Link component. The prefetching behaviour differs based on the route type:
- Static route - the whole page is prefetched and kept in the client cache for 5 minutes.
- Dynamic route - only the part of the tree up to the first
loading.tsxboundary is prefetched (if it exists), and it is not cached by default.
With Cache Components enabled, prefetching works per route rather than per link: Next.js prefetches the route's static shell (its prerendered part), and the request-specific parts stream in after the navigation.
Caching
Next.js keeps an in-memory client-side cache of RSC payloads, called the client cache (older documentation calls it the Router Cache). Once the user has visited a route, its payload is cached and reused on the next navigation, which reduces the number of requests and the amount of transferred data. We will talk about all of the caching layers later. For more information, see the caching documentation.
How long an entry stays fresh depends on how it got there: a fully prefetched (static) route is reused for 5 minutes, while a dynamic route is not reused at all by default. These durations can be tuned with the experimental staleTimes option.
Historically this cache was a common source of confusion, because it made a freshly mutated page look stale. Imagine a page listing items and another page for adding a new one. After adding an item and navigating back, you could see the cached route without the new item, even though the listing page is a server component that fetches data per request.
export default async function Page() {
const data = await readTodos();
return (
<div>
<h1>Home</h1>
<ul>
{data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
<Link href="/add-todo">Add todo Link</Link>
</div>
);
}
This
/page retrieves data from the database usingreadTodosand displays it.
export default function Page() {
return (
<div>
<Link href="/">Back to List of todos</Link>
<AddTodo />
</div>
);
}
On the other hand, this
/add-todopage allows you to add a new todo to the database (the addition is done viauseMutation, which calls the API that adds the todo). After adding a new todo, we have the option to go back to the/page, where we would ideally like to see the updated todos list, including the new todo. In this case, the only way to update the client cache, which still holds the old todos list, is to callrouter.refresh().
This is something that we can fix by using server actions, which we will briefly talk about later.
Partial rendering
Partial rendering means only the route segments that change on navigation re-render on the client, and any shared segments are preserved. Despite the similar name, this is not Partial Prerendering: PPR decides which parts of a page ship in the static shell and which stream in at request time, while partial rendering decides how much work a client-side navigation can skip.
If we access our application for the first time, page
/a, no router cache is defined, so all the code is executed in the/layout and/apage. Next, the cache for the/layout and/apage is set. When navigating between/aand/b, only the code in the/bpage is executed, but not in the/layout, since the latter is already cached.
Dynamic routes
When you don't know the exact segment names ahead of time and want to create routes from dynamic data, you can use Dynamic Segments that are filled in at request time or prerendered at build time.
A Dynamic Segment can be created by wrapping a folder's name in square brackets: [folderName]. For example, [id] or [slug]
Dynamic segments are passed as the params prop to layout, page, route and generateMetadata functions.
Think about a blog website. You want a separate page for each blog post. The layout of each page is the same, except for the actual blog content. So, you can set up a dynamic route for these pages to handle different blog posts.
* app/blog/[slug]/page.tsx /blog/a { slug: 'a' }
* app/blog/[slug]/page.tsx /blog/b { slug: 'b' }
* app/blog/[slug]/page.tsx /blog/c { slug: 'c' }
Specified file path as
app/blog/[slug]/page.tsx, where the routes in the URL can look like/blog/aor/blog/b. In this case, theslugparameter is passed to the page component with the valueaorb.
export default async function BlogPostPage({
params
}: PageProps<'/blog/[slug]'>) {
const { slug } = await params;
const response = await fetch(`https://example.com/posts/${slug}`);
const post = await response.json();
return (
<div>
<h1>{post.title}</h1>
</div>
);
}
Note the two important things here. First, params is a Promise and has
to be awaited — this changed in Next.js v15. Awaiting request data is what
tells Next.js that this part of the page cannot be prerendered without a
request. Second, we did not write the type by hand: PageProps<'/blog/[slug]'>
is generated by Next.js from your actual route structure, so if you rename the
folder from [slug] to [id], TypeScript will tell you.
The same applies to searchParams, and to the params of a layout or generateMetadata — all of them are Promises that have to be awaited.
Generate Static Params
The generateStaticParams function can be used in combination with dynamic route segments to statically generate routes at build time instead of on-demand at request time.
In certain situations, you might need to utilize dynamic route segments while having advance knowledge of the routes you want to define. For instance, if you have five distinct posts and intend to create a unique page for each of them, you can employ the generateStaticParams function to precompute and generate these routes during the build process.
export async function generateStaticParams() {
const response = await fetch('https://.../posts');
const posts = await response.json();
return posts.map(post => ({
slug: post.slug
}));
}
You do not have to list every URL. A slug that generateStaticParams does not return is not an error: the visitor is served the prerendered part of the route immediately, the rest is rendered in the background once the params are known, and the next visitor to the same URL gets that upgraded result from the cache. This is what Incremental Static Regeneration means today, and we will come back to it in the next lecture.
If a slug should not exist at all, handle it where you load the data and call notFound(), which renders the closest not-found.tsx.
import { notFound } from 'next/navigation';
export default async function BlogPostPage({
params
}: PageProps<'/blog/[slug]'>) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) {
notFound();
}
return <h1>{post.title}</h1>;
}
Catch-all Segments
Dynamic Segments can be extended to catch-all subsequent segments by adding an ellipsis inside the brackets [...folderName].
* app/shop/[...slug]/page.tsx /shop/a { slug: ["a"] }
* app/shop/[...slug]/page.tsx /shop/a/b { slug: ["a", "b"] }
* app/shop/[...slug]/page.tsx /shop/a/b/c { slug: ["a", "b", "c"] }
This is a common approach when working with authentication. An auth library relies on a number of API routes that must be reachable in your application — sign in, sign out, the OAuth callback, session lookup and so on. Instead of creating a file for each of them, you hand the whole subtree over to the library with a single catch-all route. This is exactly what better-auth, the library we will use later in the course, does.
* /app/api/auth/[...all]/route.ts
import { toNextJsHandler } from 'better-auth/next-js';
import { auth } from '@/lib/auth';
export const { GET, POST } = toNextJsHandler(auth);
In this example, all requests that start with
/api/authwill be handled by this file. The handler exported by the library is internally configured to look at the rest of the path and perform the appropriate operation depending on which URL was called.
Metadata
Metadata refers to data not directly displayed on the web page but is essential for search engines, social networks, etc. Metadata can be, for example, title, description, keywords, author, etc.
- SEO - Metadata is essential for SEO because it helps search engines understand what the page is about.
- Sharing a page, not just on social networks - When you share a page on one of the social networks, metadata like open graph tags or Twitter cards help with the proper display of the page on that platform.
Next.js has thought about metadata support and makes it easy to set metadata for each page. There are two basic methods to set metadata:
- Static metadata - metadata is fixed and does not change. This metadata is set in the page code.
- Dynamic metadata - metadata can change depending on the page's content. This metadata is set using the
generateMetadatafunction.
Whether using the static or dynamic method, the metadata is always set in the page.tsx or layout.tsx file. Importantly, the metadata is always set on the server (in the server component), never on the client.
Static metadata
Static metadata is set using a metadata object exported from a page.tsx or layout.tsx file. This object contains all the necessary metadata.
* /app/page.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: '...',
description: '...',
}
export default function Page() {
...
}
Dynamic metadata
If we need to include dynamic data in the metadata and get it from the server, we can use the generateMetadata function. A typical example might be that on a /person/<id> page, we want to include in the metadata the user name that will appear on the page. Statically, however, we don't have access to the name information; we only know the user's ID. Therefore, we can get the data from the API to display it in the metadata.
* /app/person/[id]/page.tsx
import type { Metadata } from 'next'
export async function generateMetadata({
params,
}: PageProps<'/person/[id]'>): Promise<Metadata> {
const { id } = await params;
// fetch data
const response = await fetch(`https://.../${id}`);
const person = await response.json();
return {
title: person.name,
...
};
}
export default function Page() {
...
}
Next lecture
In the next lesson, we'll take a detailed look at the concept of server-side rendering and React server components. We will explain <Suspense> boundaries and streaming. We will also discuss route handlers for creating custom API endpoints. Finally, we'll look at the rules for using both server and client components.
Assignment
This week, set up routing for a TODO application in Next.js, focusing on handling loading, error, and navigation across pages like /list and /new. Ensure consistent layouts for the todo detail pages using Next.js route groups.