muni-logo

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 no 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:

Other benefits of Next.js include:

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.

https://www.joshwcomeau.com/react/server-components/ https://www.youtube.com/watch?v=VIwWgV3Lc6s

Next.js takes advantage of RSCs and enables server-side rendering. In fact, all components by default server components. This is a 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) or you can decide as a developer, that long-running operation will happen in the background and component will be streamed in once finished, while displaying loading component immediatelly. 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 SPA React application, you need to use 3rd party libraries like React router or Tankstack 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 pure React application work. In Next.js, you have freedom to choose page rendering strategy per page.

Static site generation (SSG) - whole page is rendered at build time. By default, Next.js will try to pre-render all your pages at build time (even if you have some server components with data fetching).

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.

Routing fundamentals

The skeleton of every application is routing. Unlike React, Next.js has built in file based routing. With Next.js v13, it is possible to define 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 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 build-in solution of React.

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:

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 NotFound component if the route the user accessed does not exist. If it exists and the component is an asynchronous RSC, the Loading component is displayed if specified. The' Error' component is displayed if there is an error on the page. The most external component is then the Layout component, which wraps the entire page. This means that if the page loads, the Layout is already rendered.

In the case of the /dashboard/settings nested route, note that if /dashboard/settings does not have its Loading component defined, but /dashboard does, this loading will be used when joining /dashboard/settings. At the same time, note the hierarchy, where the Layout components 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 pages is 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 app directory, if you define folder starting with _, it will be ignored by routing system (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)).

In this example, we have /dashboard, /dashboard/overview /dashboard/settings routes, all three routes share the same layout, but /dashboard and /dashboard/overview have another shared layout, which is not applied for /dashboard/settings.

Pages and layouts

You can define new page by creating page.tsx file inside some folder in app directory. All components (including page components) are by default server components. You can turn them into client components using "use client" directive at the very top of the file. As a server component, page can fetch data by simply making component async and awaiting some promise inside a 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, do not re-render. Layouts can also be nested. It always accepts a children prop, which is the page or nested layout. Layout can be either server or client component. Root layout of NextJS application is required and must be 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 useQuery hook, if you mark layout as client component). It is not possible to pass data from layout to page (or children in general). However, you can use the same fetch function in both layout and page or any other child component and that fetch will be executed just once (more on this later).

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>
  );
}

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:

Link component

It's built-in component that extends the HTML <a> tag to provide prefetching and client-side navigation between routes. It is preferred way to navigate between routes in NextJS.

import Link from 'next/link';
 
export default function Page() {
  return <Link href="/dashboard">Dashboard</Link>;
}

You might need to check, on what page the user currently is and want to mark Link as active. You can do that by using usePathname hook. As it is a hook, it must be used inside client component only.

'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 redirect function to redirect user to another page. It is useful for example when user is not authenticated and you want to redirect him to login page. This function can be used within server components only. Component does not need to return any JSX, if it's redirecting. Component 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 does (not) 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 Link component and useRouter hook allow to perform route prefetching. Link is prefetched, once page containing a Link is loaded and Link is visible in viewport. With useRouter hook, you can prefetch route using router.prefetch method.

NextJS prefetches every Link by default. You can turn it off by passing prefetch={false} prop to Link component. Prefetching behaviour is different based on route type:

CACHING

Next.js has in-memory client-side cache called the Router cache. Once user visited some page, whole route is cached (payload of RSC with data fetching). This is supposed to improve performance and reduce number of request and data transfer. We will talk about all of the caching layers later. For more information about router cache, see documentation.

It's not possible to opt out of the Router Cache.

This is a big problem, and many developers are complaining about it. If you visit a page where you have a list of items and then you go to another page, where you're able to add a new item, and after adding it, you navigate back to the page, where you see a list of items, you will get cached route, without new item (even that the page listing all items is server component and has set up fetching 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 using readTodos and displays it.

export default function Page() {
  return (
    <div>
      <Link href="/">Back to List of todos</Link>
 
      <AddTodo />
    </div>
  );
}

On the other hand, This /add-todo page allows you to add a new todo to the database (the addition is done via useMutation, 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 Router cache, which holds the old todos list, is to call router.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.

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 /a page. Next, the cache for the / layout and /a page is set. When navigating between /a and /b, only the code in the /b page 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/a or /blog/b. In this case, the slug parameter is passed to the page component with the value a or b.

type BlogPostPageProps = {
  params: {
    slug: string;
  };
};
 
export default async function BlogPostPage({ params }: BlogPostPageProps) {
  const response = await fetch(`https://example.com/posts/${params.slug}`);
  const post = await response.json();
 
  return (
    <div>
      <h1>{post.title}</h1>
    </div>
  );
}

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
  }));
}

Using dynamicSegments property, you can specify, what should happen if unknown slug is provided:

export const dynamicParams = true;
 
export async function generateStaticParams() {
  ...
}

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 using the NextAuth library. More specifically, NextAuth relies on numerous API routes that you must incorporate into your application. While the library already includes these API routes, you must instruct your application to direct any requests to NextAuth's API routes to be processed by the NextAuth library. You can achieve this by establishing a catch-all route.

* /app/api/auth/[...nextAuth]/route.ts
 
import NextAuth from "next-auth";
 
const handler = NextAuth({
  ...
})
 
export { handler as GET, handler as POST }

In this example, all requests that start with /api/auth will be handled by this file. In this file, we also import the handler from NextAuth, which is internally configured to look at the nextAuth slug and perform the appropriate operations depending on what API 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.

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:

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/page.tsx
 
import type { Metadata } from 'next'
 
export async function generateMetadata({
  params,
}: {
  params: { id: string };
}): Promise<Metadata> {
  const id = params.id;
 
  // 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.