Migrating to Next.js Server Components Layouts

A simple guide to migrating your _app.tsx component to the new Server Components released with Next.js 13

Vercel has finally shipped Next.js 13, and with it one of the most requested features: nested Layouts using the new React 18 Server Components, powered by React Suspense.

The changes and possibilities Next.js 13 brings are revolutionary: thanks to the new Server Components and seamless Suspense integration, Next. js applications will be smaller and faster.

With that said, transitioning from the good old _app.tsx root component to Server Components won't be the simplest: the new Next.js 13 Layouts are basically a whole new framework.

This blog post is ideal for those who are unsure how to migrate their apps from the old _app.tsx to using the new Layouts Server Components.

Bye Bye _app.tsx and _document.tsx

So, are we deleting _app.tsx and _document.tsx? Yes. These are no longer needed when working with the new Next.js app directory. Gone. Done. Ciao.

This introduces lots of questions:

  1. How do we pass data to a component?
  2. How do we initialize our applications?
  3. Where do we place our app-wide Providers?

Believe me, I was as confused as you are, and still am while writing this. But let's go on.

The general rule that seems to be pushed by the Next.js team is: fetch the data in every component that needs it, and use the provided utilities to ensure dedupe and avoid multiple calls.

While requests made with fetch are automatically deduped and cached, it's not the same when using different data-fetching utilities: for example, ORMs, Firestore, and so on. In that case, we're going to use the React.js cache utility.

Initializing the application's state

The old _app.tsx was handy when initializing app-wide state using providers: since we were able to fetch the current page's props, we could retrieve data from the server (for example, the current user) and hydrate the application's data using Contexts, our other types of state management. It's simply a very common pattern, that now no longer works the same way.

Normally, we can initialize the application's providers using the root layout, and it will work in the same way as the old _app.tsx root component.

Unfortunately, if your application relies on data that needs to be passed down from the server's response, we can not use the root layout since this doesn't get the data passed from the page component: Layout components will only be wrappers for the page components, e.g. their children props. Tricky, isn't it?

This is why the logic to initialize a page needs to be fetched by each page component.

Hydrating the Application's State: a practical example

Okay, enough talk, let's write some code.

What's a popular scenario for real server-side rendered applications? One of them is passing the current signed-in user to the page and hydrating a global context with the user's data. This allows us to access the user's object throughout the application.

In the code below we abstract some implementation details such as getCurrentUser: simply assume this function gets the current user using a cookie, and it will throw if it fails.

import { use } from 'react';
import { redirect } from 'next/navigation';
import { cookies } from "next/headers";
function Page() {
const { data, action } = use(getPageData());
if (action) {
return action();
}
return <UserPage user={data} />;
}
async function getPageData() {
const session = cookies().get('session');
const redirectToLogin = () => redirect('/auth/sign-in');
if (!session) {
return {
data: undefined,
action: redirectToLogin,
};
}
try {
const data = await getUserBySessionId(session);
return {
data,
};
} catch (e) {
return {
data: undefined,
action: redirectToLogin,
};
}
}
export default Page;

The above can definitely get way more complex in real applications, especially if type-safe, but it should give you an idea of how you can reuse functions across your pages and return a set of common data to your components.

The above takes care of fetching the data using a reusable function, but does not show how to pass this down to a context function. To do that, we need a client component, since we're going to use React's hooks such as useContext.

To mark a component as a client component, Next.js uses a special string use client at the top of the component's file:

`use client`;
export default function Component() {}

So, we create a UserContext using the Context API to hold the state of the signed in user.

import { createContext } from 'react';
import { User } from './types';
const UserContext = createContext<User | undefined>(undefined);
export default UserContext;

Now we're going to define the component UserPage and pass the value retrieved from the page.

Just to test that it works, we import the context using useContext to display the current user's name using UserRenderer:

'use client';
import { useContext } from 'react';
import { User } from './types';
import UserContext from './UserContext';
function UserPage(
props: React.PropsWithChildren<{
user: User;
}>
) {
return (
<UserContext.Provider value={props.user}>
<UserRenderer />
</UserContext.Provider>
);
}
function UserRenderer() {
const user = useContext(UserContext);
return <div>Ciao, {user?.displayName}</div>;
}
export default UserPage;

Migrating from getServerSideProps

Reading the URL Params

The page components receive props with two properties: params and searchParams:

  1. params are the page's segments
  2. queryParams are the URL's query params

You can use these components to replace getServerSideProps's ctx's parameters.

To read your query params, check the component below, that reads the query parameter from the URL:

function SearchPage(props: { searchParams: Record<string, string> }) {
const query = props.searchParams.query;
if (!query) {
return redirect('/');
}
const data = use(getResultsFromQuery({ query, page: 0 }));
// render function

To read the page's segments, instead, you can use params:

function StoryPage(props: { params: Record<string, string> }) {
const id = props.params.id;
if (!id) {
return redirect('/');
}
const item = use(getItemById(id));
const comments = use(getCommentsByStoryId(id));
// render function

Reading Cookies and Headers

To read cookies and headers use the imports from the next/headers package:

import { cookies, headers } from 'next/headers';
async function getPageData() {
const session = cookies().get('session');
const csrfToken = headers().get('csrfToken');
}

Redirecting to other Pages

As we've seen above, you can use the redirect function from the new next/navigation package to replace { redirect: { destination } } props that you used to use in getServerSideProps.

import { redirect } from 'next/navigation';
import { cookies } from 'next/headers';
function Page() {
const organizationId = cookies.get('organizationId');
if (!organizationId) {
return redirect('/auth/sign-in');
}
}

Redirecting when the page is not found

To redirect users to your not-found.tsx page, you need to use notFound from the next/navigation package:

import { notFound } from 'next/navigation';
import { cookies } from 'next/headers';
function Page() {
const id = cookies.get('organizationId');
const organization = use(getOrganization(id));
if (!organization) {
return notFound();
}
}

And this it! This should give you a good idea of how the new utilities in Next.js 13 can help you migrate from Next.js 12's getServerSideProps to the new Server Components pages.

As I'm still learning and experimenting with this, please bear in mind there could be mistakes. I will keep updating these articles as I uncover more about Next.js 13 Layouts and Server Components. Ciao!