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:
- How do we pass data to a component?
- How do we initialize our applications?
- 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
:
params
are the page's segmentsqueryParams
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!