Hi and welcome! In this tutorial, we'll learn how to use Next.js 13 with server components and the app directory.
What does Next.js 13 bring to the table?
Next.js 13 is a major release of the most popular React framework: in fact, it ships with a new routing system, also called App Router. In many ways, this new routing system is a complete rewrite of the previous one, and fundamentally changes the way we will write Next.js applications.
The new app
directory brings many improvements to the existing pages
directory, and it's the new default way to write Next.js apps, after a period of experimental releases.
Server Components (RSC)
The biggest change in the new App Router is the introduction of Server Components, a new type of React components that run on the server and return compiled JSX that is sent to the client. Server Components are useful for rendering the skeleton of a page, fetching data from the server in parallel, and passing it to the "client components".
Server Components are the default component type in the new app
directory.
Layouts
Powered by Server Components, layouts are foundational components that wrap pages. Next.js Layouts are not only useful for displaying a common UI across pages, but also to reuse logic and data fetching across pages.
Layouts also solve the waterfall problem, which is a common issue with the current Next.js routing system. In fact, with the new App Router, we can fetch data in parallel, and pass it to the page component: this is a substantial performance improvement over the current routing system.
Server Actions
Still in alpha, Server Actions are a new way to execute functions on the server, instead of wiring them through an API handler. Server actions are useful for executing server-side logic, such as sending emails, or updating a database. Server actions are useful for submitting forms, execute server-side logic, and redirect the user to a new page.
Additionally, server actions offer us the ability to revalidate data fetched from Server Components, eliminating the need for complex client-side state management as a result of data mutation (such as updating a Redux store).
We introduce Server Actions in this tutorial, but we cover them in detail in a separate article, linked at the end of this tutorial.
Enhanced Router
Using conventional filenames, we can add various types of components in the app
directory. This is a big improvement over the current routing system, which requires us to use a specific directory structure to define pages, API handlers, and so on.
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
- templates are defined as
template.tsx
- errors are defined as
error.tsx
- loading states are defined as
loading.tsx
- not found pages are defined as
not-found.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 13, 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>;}
Defining Client components
On the contrary, in the Next.js app directory, we need to specifically define client components.
We can do this by specifying the use client
pragma at the top of the file:
'use client';export default function ClientComponent() { return <div>Client 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 13 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.
Next.js 13 Folder Structure
What does the new Next.js 13 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 13 app with the new app
directory:
- app - layout.tsx - (site) - page.tsx - layout.tsx - app - dashboard - page.tsx - layout.tsx
As you can see, the name of the file reflects the type of component it is. For example, layout.tsx
is a layout component, while page.tsx
is a page component, and so on.
Don't worry, we will go through all the different types of components in the next sections.
Co-locating files in Next.js Routing Structure
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. Unlike the older pages
router, we can define any type of component in the app
directory without it becoming a page.
For example, we could place our components for a specific page right in the folder where it's defined:
- app - (site) - components - Dashboard.tsx - hooks - use-fetch-data-hook.ts - page.tsx
Side-note: why is (site)
in parentheses? By using parentheses, we make the directory site
"pathless", which means we can create new layouts, loading files, and pages in the site
directory, without adding a new path segment to the routing.
All pages under (site)
will be accessed from the root path /
: for example, the page app/(site)/page.tsx
will be accessible at /
.
Next.js 13 folder structure best practices
When it comes to the new app
directory, there are some best practices we can follow to make our codebase more maintainable, and to make it easier to migrate to the new directory structure.
Place files close to where they are used
So, where to put components in Next.js 13? The answer is: it depends.
Since we can colocate files in the app
directory, we can place files close to where they are used. For example, we can place a component in the same directory as the page that uses it:
- app - (site) - page.tsx - components - PageComponent.tsx
As you can see from the above, we are able to place the PageComponent
in the same directory as the page that uses it. This is a big improvement over the current pages
directory, where we need to place components in a separate directory.
This is not the whole story, though. In fact, we can also place components in a separate directory, if we want to reuse them across multiple pages: read on to learn more about this.
When to not colocate components and files in Next.js 13
In many cases, your components or libraries are global, eg. they are used across multiple pages. In this case, you can place them in the app
directory, or in a separate directory such as app/components
, app/lib
, etc.
Since they may need to be used across different places - it makes sense to place them in a separate directory that is shared across the app.
This is exactly how we have structured our SaaS Starter Kit.
For example, we have a components
directory that contains all the components used across the app, a lib
directory that contains all the libraries used across the app, and so on:
- src - app - layout.tsx - (site) - page.tsx - layout.tsx - components - HomePage.tsx - HomePageNewsletterInput.tsx - components - Button.tsx - Input.tsx - lib - api.ts - auth.ts - storage.ts
When to use route groups
Route groups are a way to group pages under a common path segment or a common layout, or separating some pages from a layout without affecting the resulting URL. For example, if we want to group all pages under the /dashboard
path, we can use a route group:
- app - (dashboard) - page.tsx - layout.tsx - analytics - page.tsx - billing - page.tsx
In the cases above, the pages under (dashboard)
will be accessible under the root /
path. For example, the page app/(dashboard)/analytics/page.tsx
will be accessible at /analytics
.
With that said - these pages will share the same layout, which is defined in app/(dashboard)/layout.tsx
, which we can use to define a common layout for all pages under the (dashboard)
route group - or to load some data needed in all pages.
Layouts
Next.js Layouts are one of the biggest new functionality made possible by the new App Router.
Next.js 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;}
Alternatively - you can also use the function cookies().set
to set cookies in API routes and Server Actions:
import { cookies } from 'next/headers';export async function POST( request: Request) { const organizationId = getOrganizationId(); cookies().set('organizationId', organizationId, { path: '/', httpOnly: true, sameSite: 'lax', }); return NextResponse.json({ organizationId });}
NB: you cannot set a cookie from a Server Component. The Server component below will throw an error:
import { cookies } from 'next/headers';export default function ServerComponent() { cookies().set('organizationId', organizationId, { path: '/', httpOnly: true, sameSite: 'lax', }); return <div>Server Component</div>;}
Redirecting from API Routes
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. Handling webhooks in Next.js App Router to retrieve the raw body from the request is much simpler than in the Pages routing system.
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 13. 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 aform
component - Calling the action from a
button
component using theformAction
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)
Learn more about Server Actions
If you want to know more about Server Actions, check out our article on Next.js Server Actions.
Reusing computation in Server Components using the "cache" utility
React now provides a new utility called cache
that allows us to reuse computation in Server Components.
Since Server Components are rendered in parallel on the server, we can use the cache
utility to reuse computation across components.
For example, assume you have 2 layouts fetching the current user from the server - you can use the cache
utility to reuse the computation:
import { cache } from 'react';export const getUser = cache((userId: string) => { return client.auth.getUserById(userId);});
Of course - you will replace client.auth.getUserById
with your own function to fetch the user from the server. This could be different depending on the authentication provider you are using.
But what's important to know is that the cache
utility will cache the result of the function, and reuse it across components - using the userId
as the cache key. If a different key is passed, the function will be called again.
What's the cache lifetime? The cache lifetime is the request. React will clear the cache after the request is completed - so you don't need to worry about stale data.
Conclusion
In this article, we learned how to use the new experimental (Next.js App Router is no longer experimental as of version 13.4.0) App Router in Next.js 13.
The patterns and conventions we learned in this article are still experimental, and may change in the future. However, they are already very useful, and we can already start using them in our projects.
Did you know you can our Next.js 13 SaaS Starter Kit to build your own SaaS app? It's a fully-featured SaaS Starter Kit that includes everything you need to build a SaaS app with Next.js 13, including authentication, billing, and more.
Our Supabase Next.js SaaS Starter uses Supabase for the backend functionalities such as authentication, storage and a Postgres database.
While it's still experimental, you can start building your SaaS app with Next.js 13 today and future-proof your app for the future, without the need of painful migrations from the old architecture.