Introduction to Server Components

February 5, 2024
·
views
·
likes

The App Directory: A New Era for Next.js Development

The App Directory supersedes the traditional pages directory, representing the next step in Next.js app development post its experimental phase.

Introducing Server Components (RSC)

One of the most amazing things about the App Router is the arrival of Server Components. These special React parts work only on the server side, creating JSX code that's sent to the user's browser. With Server Components, we can quickly show the basic structure of a webpage, fetch data from the server, and smoothly connect with other parts that run on the user's device.

Elevating Layouts to New Heights

Powered by Server Components, layouts emerge as pivotal building blocks enveloping pages. Beyond mere UI consistency, layouts facilitate code reuse and data fetching across multiple pages, effectively mitigating the notorious waterfall problem inherent in previous routing systems.

Unveiling Server Actions

Server Actions herald a revolutionary approach to server-side logic execution. By sidestepping traditional API handlers, Server Actions empower developers to perform server-side tasks such as email dispatch or database updates with unprecedented ease. Additionally, they offer seamless data revalidation post-fetch, obviating the need for intricate client-side state management.

A Solution to the Waterfall Problem

Layouts offer a remedy for the notorious "waterfall problem" that affects the traditional Next.js routing system. Thanks to the innovative App Router, layouts now enable simultaneous data fetching, which significantly enhances performance and rendering speed.

Introducing Next.js Server Actions

At the beginning, Server Actions are a new tool that lets us do tasks on the server easily, like sending emails or updating information in a database. They are different from the usual way of handling these tasks because they make it possible to do server work directly, making things like submitting forms and moving from one page to another on the server smoother. They also help keep data up-to-date. This makes managing data on the server better and simpler, without the need for complex ways of managing data on the user's side.

Revolutionizing Routing with Next.js Enhanced Router

Gone are the days of cumbersome directory structures dictating component placement. With Next.js Enhanced Router, developers wield the power of conventional filenames to seamlessly integrate various component types within the app directory. This paradigm shift unlocks unparalleled flexibility, liberating developers from the shackles of rigid routing conventions.

What does this mean? From now on, we can create various types of components in the app directory using a specific filename convention specific to Next.js:

pages are defined as page.tsx
layouts are defined as layout.tsx
errors are defined as error.tsx
loading states are defined as loading.tsx

What are Server Components? Server Components are a new type of React components that run on the server and return compiled JSX that is sent to the client. Next.js, with its new app directory released in Next.js , fully embraced Server Components by making them the default type components.

This is a big shift from traditional React components that run both on the server and on the client. In fact, as we have specified, React Server components do not execute on the client.

As such, there are some constraints to using Server Components that we need to keep in mind:

Server components cannot use browser-only APIs Server components cannot use React hooks Server components cannot use Context So, what are they for?

React Server Components are useful for rendering the skeleton of a page, while leaving the interactive bits to the so-called "client components".

Despite their name, "client components" (which, IMHO, is unfortunate) are also server-rendered, and they run on both the server and the client.

React Server Components can be useful because they allows us to:

render pages faster reduce the amount of JavaScript that needs to be sent to the client improve the routing performance of server-rendered pages In short, we use Server Components to fetch data from the server and render the skeleton of a page: then, we can pass the data to the "client components".

Server Components vs Client Components As we have seen, Server Components are useful for rendering the skeleton of a page, while Client Components are the components as we know them today.

This comparison in the Next.js docs is a good way to understand the difference between the two.

Defining Server Components Server components do not need a notation to be defined as such: server components are the default components when rendered in the app directory.

We cannot use React hooks, Context, or browser-only APIs in Server Components. However, we can use Server Components only APIs, such as headers, cookies, etc.

Server components can import client components.

There is no need to specify a notation to define a Server Component: in fact, Server Components are the default component type in the new app directory.

Assuming the component ServerComponent is not a child of a Client Component, it will be rendered on the server and sent to the client as compiled JSX:

export default function ServerComponent() {
  return <div>Server Component</div>;
}

When we use client components, we can use React hooks, Context, and browser-only APIs. However, we cannot use some Server Components only APIs, such as headers, cookies, etc.

NB: Client components cannot import server components, but you can pass a Server Component as a child or prop of a Client Component.

App directory The new "app" directory released in Next.js is an experimental a new way to build Next.js apps. It coexists with the pages directory, and we can use it to incrementally migrate an existing project to the new directory structure.

This new directory structure is not just a new way to write apps, it's a whole new routing system underneath, much more powerful than the current one.

File Structure What does the new Next.js file structure look like? Let's take a look at the example app we'll be using in this tutorial.

Below is an example of a Next.js app with the new app directory:

Don't worry, we will go through all the different types of components in the next sections.

Colocation One important side-effect of the new app directory is that it allows us to colocate our files. Since filenames are conventional, we can define any file in the app directory without these becoming pages components.

For example, we could place our components for a specific page right in the folder where it's defined:

All pages under (site) will be accessed from the root path /: for example, the page app/(site)/page.tsx will be accessible at /.

Layouts Layouts are one of the biggest new functionality made possible by the new App Router.

Layouts are foundational components that wrap pages: this is not only useful for displaying a common UI across pages, but also to reuse data-fetching and logic.

Next.js needs one root layout component:

export const metadata = {
  title: 'Next.js Tutorial',
  description: 'A Next.js tutorial using the App Router',
};

async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang={'en'}>
      <body>{children}</body>
    </html>
  );
}

export default RootLayout;

Layouts are defined using the convention layout.tsx in the app directory: Next.js will automatically wrap all pages within the folder where the layout is defined.

For example, if we have a layout defined in app/(site)/layout.tsx, Next.js will wrap all pages in the app/(site) directory with this layout:

export default async function SiteLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div>
      <main>
        {children}
      </main>
    </div>
  );
}

As a result - all the pages in the app/(site) directory will be wrapped with the SiteLayout component.

Loading data in Layout Components Layout components can be extremely useful also in case you need to load some data needed in all pages of a directory: for example, we could load the user's profile in the layout component, and pass it to the page components.

To fetch data in Layout Components in Next.js, we can use the new use hook, an experimental hook in React that uses Suspense to fetch data on the server.

import { use } from "react";

export default function SiteLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const data = use(getData());

  return (
    <div>
      <header>
        { data.user ? <ProfileDropown /> : null }
      </header>

      <main>
        {children}
      </main>
    </div>
  );
}

function getData() {
  return fetch('/api/data').then(res => res.json());
}

In the example above:

we fetch the data in the layout component using the use hook we conditionally render the ProfileDropdown component based on the data.user property NB: we used the use hook to fetch the data in a (seemingly) synchronous way. This is because the use hook uses Suspense under the hood, which allows us to write asynchronous code in a synchronous way.

Using Async/Await in Server Components An alternative way would be to make the component an async component, and use async/await to fetch the data from getData:

export default async function SiteLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const data = await getData()

  return (
    <div>
      <header>
        { data.user ? <ProfileDropown /> : null }
      </header>

      <main>
        {children}
      </main>
    </div>
  );
}

function getData() {
  return fetch('/api/data').then(res => res.json());
}

Reading Cookies and Headers If you're using a Server Component, you can read cookies and headers using from the next/headers package.

NB: At the time of writing, we can only use these functions to read their values, but not to set or delete them.

import { cookies } from 'next/headers';

export function Layout(
  { children }: { children: React.ReactNode },
) {
  const lang = cookies.get('lang');

  return (
    <html lang={lang}>
      <body>
        {children}
      </body>
    </html>
  );
}

If you feel like something is missing, don't worry, it's not just you. In fact, unlike getServerSideProps, we do not have access to the request object. This is why Next.js is exposing these utilities to read data from the request.

Redirecting from Layouts In layouts, we can also redirect users to a different page.

For example, if we want to redirect users to the login page if they are not authenticated, we can do it in the layout component:

import { use } from 'react';
import { redirect } from 'next/navigation';

function AuthLayout(
  props: React.PropsWithChildren,
) {
  const session = use(getSession());

  if (session) {
    return redirect('/dashboard');
  }

  return (
    <div className={'auth'}>
      {props.children}
    </div>
  );
}


function getSession() {
  return fetch('/api/session').then(res => res.json());
}

Now, we can use the loadSession function in the layout component:

import { use } from 'react';

function AuthLayout(
  props: React.PropsWithChildren,
) {
  const response = use(loadSession());
  const data = response.data;

  // do something with data

  return (
    <div className={'auth'}>
      {props.children}
    </div>
  );
}

Using the redirect side-effect in Next.js The new Next.js function redirect will throw an error: in fact, its return type is never. If you catch the error, you need to be careful and ensure to follow the redirect thrown by the error.

To do that, we can use some utilities exported by the Next.js package:

import { use } from 'react';

import {
  isRedirectError,
  getURLFromRedirectError,
} from 'next/dist/client/components/redirect';

import { redirect } from "next/navigation";

async function loadData() {
  try {
    const data = await getData();

    if (!data) {
      return redirect('/login');
    }

    const user = data.user;

    console.log(`User ${user.name} logged in`);

    return user;
  } catch (e) {
    if (isRedirectError(e)) {
      return redirect(getURLFromRedirectError(e));
    }

    throw e;
  }
}

function Layout(
  props: React.PropsWithChildren,
) {
  const data = use(loadData());

  // do something with data

  return (
    <div>
      {props.children}
    </div>
  );
}

Pages To define pages in the new app directory, we use the special convention page.tsx. That means, if we want to define a page in the app directory, we need to name the file page.tsx.

For example, if we want to define the home page of your website, we can place the page in the app/(site) directory and name it page.tsx:

function SitePage() {
  return <div>Site Page</div>;
}

export default SitePage;

Page Metadata and SEO To specify the metadata of a page, we can export the constant metadata property in the page.tsx file:

export const metadata = {
  title: 'Site Page',
  description: 'This is the site page',
};
If you need to access dynamic data, you can use the generateMetadata function:
export async function generateMetadata(
  { params, searchParams }
) {
  return { title: '...' };
}

Check out the Next.js documentation for the full list of supported metadata properties.

Generating Static Pages To generate a list of static pages to be used with dynamic parameters, we can use the generateStaticParams function:

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

  return posts.map((post) => ({
    slug: post.slug,
  }));
}

Check out the full documentation for generating static paths.

Loading Indicators When navigation between pages, we may want to display a loading indicator. To do this, we can use the loading.tsx file which we can define in every directory:

export default function Loading() {
  return <div>Loading...</div>;
}

Here you can add any component you want to display while the page is loading, such as a top bar loader, or a loading spinner, or both.

Error Handling At the moment, you can define a "not found" page using the convention not-found.tsx:

export default function NotFound() {
  return (
    <>
      <h2>Not Found</h2>
      <p>Could not find requested resource</p>
    </>
  );
}

This file will only be displayed if used in conjunction with the notFound function. This is why it's still recommended to use custom 400 and 500 pages using the old pages directory.

Custom 404 and 500 pages At the time of writing, we need to stick with the regular pages directory to define custom 404 and 500 pages. This is because Next.js does not support custom 404 and 500 pages in the app directory.

Fonts We can use the package next/font to load fonts in our application.

To do so, we need to define a client component, and import it in the root layout app/layout.tsx file:

'use client';

import { Inter } from 'next/font/google';
import { useServerInsertedHTML } from 'next/navigation';

const heading = Inter({
  subsets: ['latin'],
  variable: '--font-family-heading',
  fallback: ['--font-family-sans'],
  weight: ['400', '500'],
  display: 'swap',
});

export default function Fonts() {
  useServerInsertedHTML(() => {
    return (
      <style
        dangerouslySetInnerHTML={{
          __html: `
          :root {
            --font-family-sans: '-apple-system', 'BlinkMacSystemFont',
              ${sans.style.fontFamily}, 'system-ui', 'Segoe UI', 'Roboto',
              'Ubuntu', 'sans-serif';

            --font-family-heading: ${heading.style.fontFamily};
          }
        `,
        }}
      />
    );
  });

  return null;
}

After that, we can import the Fonts component in the root layout:

import Fonts from '~/components/Fonts';

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html>
      <Fonts />

      <body>{children}</body>
    </html>
  );
}

API Routes The new app directory also supports API routes. The convention to define an API route is to create a file named route.tsx in the app directory.

API routes now use the standard Request object rather than the express-like req and res objects.

When we define an API route, we can export the handler for the methods we want to support. For example, if we want to support the GET and POST methods, we can export the GET and POST functions:

import { NextResponse } from 'next/server';

export async function GET() {
  return NextResponse.json({ hello: 'world' });
}

export async function POST(
  request: Request
) {
  const body = await request.json();
  const data = await getData(body);

  return NextResponse.json(data);
}

If we want to manipulate the response, for example by setting cookies, we can use the NextResponse object:

export async function POST(
  request: Request
) {
  const organizationId = getOrganizationId();
  const response = NextResponse.json({ organizationId });

  response.cookies.set('organizationId', organizationId, {
    path: '/',
    httpOnly: true,
    sameSite: 'lax',
  });

  return response;
}

In API routes, just like in Server Components, we can also redirect users using the redirect function imported from next/navigation:

import { redirect } from 'next/navigation';

export async function GET(
  request: Request
) {
  return redirect('/login');
}

Handling Webhooks Handling webhooks is a common use case for API routes, and getting the raw body request is now much simpler. In fact, we can get the raw body request by using the request.text() method:

export async function POST(
  request: Request
) {
  const rawBody = await request.text();

  // handle webhook here
}

Server Actions

Server Actions are a new concept introduced in Next.js . They are a way to define server-side actions that can be called from the client. All you need to do is to define a function and use the use server keyword at the top:

For example, the below is a valid server action:

async function myActionFunction() {
"use server";
 
  // do something
}

If you are defining a server action from a client component, this needs to be exported from a separate file, and imported in the client component. The file needs the keyword use server at the top:

 'use server';

async function myActionFunction() {
  // do something
}

To call the server action from the client, you have multiple ways

Defining the action as the action property of a form component Calling the action from a button component using the formAction property Calling the action using the useTransition hook (if it mutates data) Simply calling the action like a normal function (if it does not mutate data) If you want to know more about Server Actions, I will soon post a whole article dedicated to them soon.

Conclusion In this article, we learned how to use the new App Router in Next.js .

While Next.js App Router is now out of beta, some of the patterns and conventions we learned in this article are new and may be still not production-ready, and may change in the future.

However, they are already very useful, and we can already start using them in our projects.

LikeButton
Footer