Skip to content
Back to Blog

Monday, May 23rd 2022

Layouts RFC

Posted by

This RFC (Request for Comment) outlines the biggest update to Next.js since it was introduced in 2016:

  • Nested Layouts: Build complex applications with nested routes.
  • Designed for Server Components: Optimized for subtree navigation.
  • Improved Data Fetching: Fetch in layouts while avoiding waterfalls.
  • Using React 18 Features: Streaming, Transitions, and Suspense.
  • Client and Server Routing: Server-centric routing with SPA-like behavior.
  • 100% incrementally adoptable: No breaking changes so you can adopt gradually.
  • Advanced Routing Patterns: Parallel routes, intercepting routes, and more.

The new Next.js router will be built on top of the recently released React 18 features. We plan to introduce defaults and conventions that allow you to easily adopt these new features and take advantage of the benefits they unlock.

Work on this RFC is ongoing and we'll announce when the new features are available. To provide feedback, join the conversation on Github Discussions.

Table of Contents

Motivation

We've been gathering community feedback from GitHub, Discord, Reddit, and our developer survey about the current limitations of routing in Next.js. We've found that:

  • The developer experience of creating layouts can be improved. It should be easy to create layouts that can be nested, shared across routes, and have their state preserved on navigation.
  • Many Next.js applications are dashboards or consoles, which would benefit from more advanced routing solutions.

While the current routing system has worked well since the beginning of Next.js, we want to make it easier for developers to build more performant and feature-rich web applications.

As framework maintainers, we also want to build a routing system that is backwards compatible and aligns with the future of React.

Note: Some routing conventions were inspired by the Relay-based router at Meta, where some features of Server Components were originally developed, as well as client-side routers like React Router and Ember.js. The layout.js file convention was inspired by the work done in SvelteKit. We'd also like to thank Cassidy for opening an earlier RFC on layouts.

Terminology

This RFC introduces new routing conventions and syntax. The terminology is based on React and standard web platform terms. Throughout the RFC, you'll see these terms linked back to their definitions below.

  • Tree: A convention for visualizing a hierarchical structure. For example, a component tree with parent and children components, a folder structure, etc.
  • Subtree Part of the tree, starting at the root (first) and ending at the leaves (last).
  • URL Path: Part of the URL that comes after the domain.
  • URL Segment: Part of the URL path delimited by slashes.

How Routing Currently Works

Today, Next.js uses the file system to map individual folders and files in the Pages directory to routes accessible through URLs. Each page file exports a React Component and has an associated route based on its file name. For example:

Introducing the app Directory

To ensure these new improvements can be incrementally adopted and avoid breaking changes, we are proposing a new directory called app.

The app directory will work alongside the pages directory. You can incrementally move parts of your application to the new app directory to take advantage of the new features. For backwards compatibility, the behavior of the pages directory will remain the same and continue to be supported.

Defining Routes

You can use the folder hierarchy inside app to define routes. A route is a single path of nested folders, following the hierarchy from the root folder down to a final leaf folder.

For example, you can add a new /dashboard/settings route by nesting two new folders in the app directory.

Note:

  • With this system, you'll use folders to define routes, and files to define UI (with new file conventions such as layout.js, page.js, and in the second part of the RFC loading.js).
  • This allows you to colocate your own project files (UI components, test files, stories, etc) inside the app directory. Currently this is only possible with the pageExtensions config.

Route Segments

Each folder in the subtree represents a route segment. Each route segment is mapped to a corresponding segment in a URL path.

For example, the /dashboard/settings route is composed of 3 segments:

  • The / root segment
  • The dashboard segment
  • The settings segment

Note: The name route segment was chosen to match the existing terminology around URL paths.

Layouts

New file convention: layout.js

So far, we have used folders to define the routes of our application. But empty folders do not do anything by themselves. Let's discuss how you can define the UI that will render for these routes using new file conventions.

A layout is UI that is shared between route segments in a subtree. Layouts do not affect URL paths and do not re-render (React state is preserved) when a user navigates between sibling segments.

A layout can be defined by default exporting a React component from a layout.js file. The component should accept a children prop which will be populated with the segments the layout is wrapping.

There are 2 types of layouts:

  • Root layout: Applies to all routes
  • Regular layout: Applies to specific routes

You can nest two or more layouts together to form nested layouts.

Root Layout

You can create a root layout that will apply to all routes of your application by adding a layout.js file inside the app folder.

Note:

  • The root layout replaces the need for a custom App (_app.js) and custom Document (_document.js) since it applies to all routes.
  • You'll be able to use the root layout to customize the initial document shell (e.g. <html> and <body> tags).
  • You'll be able to fetch data inside the root layout (and other layouts).

Regular Layouts

You can also create a layout that only applies to a part of your application by adding a layout.js file inside a specific folder.

For example, you can create a layout.js file inside the dashboard folder which will only apply to the route segments inside dashboard.

Nesting Layouts

Layouts are nested by default.

For example, if we were to combine the two layouts above. The root layout (app/layout.js) would be applied to the dashboard layout, which would also apply to all route segments inside dashboard/*.

Pages

New file convention: page.js

A page is UI that is unique to a route segment. You can create a page by adding a page.js file inside a folder.

For example, to create pages for the /dashboard/* routes, you can add a page.js file inside each folder. When a user visits /dashboard/settings, Next.js will render the page.js file for the settings folder wrapped in any layouts that exist further up the subtree.

You can create a page.js file directly inside the dashboard folder to match the /dashboard route. The dashboard layout will also apply to this page:

This route is composed of 2 segments:

  • The / root segment
  • The dashboard segment

Note:

  • For a route to be valid, it needs to have a page in its leaf segment. If it doesn't, the route will throw an error.

Layout and Page Behavior

  • The file extensions js|jsx|ts|tsx can be used for Pages and Layouts.
  • Page Components are the default export of page.js.
  • Layout Components are the default export of layout.js.
  • Layout Components must accept a children prop.

When a layout component is rendered, the children prop will be populated with a child layout (if it exists further down the subtree) or a page.

It may be easier to visualize it as a layout tree where the parent layout will pick the nearest child layout until it reaches a page.

Example:

app/layout.js
// Root layout
// - Applies to all routes
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Header />
        {children}
        <Footer />
      </body>
    </html>
  );
}
app/dashboard/layout.js
// Regular layout
// - Applies to route segments in app/dashboard/*
export default function DashboardLayout({ children }) {
  return (
    <>
      <DashboardSidebar />
      {children}
    </>
  );
}
app/dashboard/analytics/page.js
// Page Component
// - The UI for the `app/dashboard/analytics` segment
// - Matches the `acme.com/dashboard/analytics` URL path
export default function AnalyticsPage() {
  return <main>...</main>;
}

The above combination of layouts and pages would render the following component hierarchy:

Component hierarchy
<RootLayout>
  <Header />
  <DashboardLayout>
    <DashboardSidebar />
    <AnalyticsPage>
      <main>...</main>
    </AnalyticsPage>
  </DashboardLayout>
  <Footer />
</RootLayout>

React Server Components

Note: React introduced new component types: Server, Client (traditional React components), and Shared. To learn more about these new types, we recommend reading the React Server Components RFC.

With this RFC, you can start using React features and incrementally adopt React Server Components into your Next.js application.

The internals of the new routing system will also leverage recently released React features such as Streaming, Suspense, and Transitions. These are the building blocks for React Server Components.

Server Components as the Default

One of the biggest changes between the pages and app directories is that, by default, files inside app will be rendered on the server as React Server Components.

This will allow you to automatically adopt React Server Components when migrating from pages to app.

Note: Server components can be used in the app folder or your own folders, but cannot be used in the pages directory for backwards compatibility.

Client and Server Components Convention

The app folder will support server, client, and shared components, and you'll be able to interleave these components in a tree.

There is an ongoing discussion on what exactly the convention will be for defining Client Components and Server Components. We will follow the resolution of this discussion.

  • For now, Server components can be defined by appending .server.js to the filename. E.g. layout.server.js
  • Client components can be defined by appending the .client.js to the filename. E.g. page.client.js.
  • .js files are considered shared components. Since they could be rendered on the Server and the Client, they need to respect the constraints of each context.

Note:

  • Client and Server components have constraints that need to be respected. When deciding to use a client or server component, we recommend using server components (default) until you need to use a client component.

Hooks

We'll be adding Client and Server component hooks that’ll allow you to access the headers object, cookies, pathnames, search params, etc. In the future, we'll have documentation with more information.

Rendering Environments

You'll have granular control of what components will be in the client-side JavaScript bundle using the Client and Server Components convention.

By default, routes in app will use Static Generation, and will switch to dynamic rendering when a route segment uses server-side hooks that require request context.

Interleaving Client and Server Components in a Route

In React, there's a restriction around importing Server Components inside Client Components because Server Components might have server-only code (e.g. database or filesystem utilities).

For example, importing the Server Component would not work:

ClientComponent.js
import ServerComponent from './ServerComponent.js';
 
export default function ClientComponent() {
  return (
    <>
      <ServerComponent />
    </>
  );
}

However, a Server Component can be passed as a child of a Client Component. You can do this by wrapping them in another Server Component. For example:

ClientComponent.js
export default function ClientComponent({ children }) {
  return (
    <>
      <h1>Client Component</h1>
      {children}
    </>
  );
}
 
// ServerComponent.js
export default function ServerComponent() {
  return (
    <>
      <h1>Server Component</h1>
    </>
  );
}
 
// page.js
// It's possible to import Client and Server components inside Server Components
// because this component is rendered on the server
import ClientComponent from "./ClientComponent.js";
import ServerComponent from "./ServerComponent.js";
 
export default function ServerComponentPage() {
  return (
    <>
      <ClientComponent>
        <ServerComponent />
      </ClientComponent>
    </>
  );
}

With this pattern, React will know it needs to render ServerComponent on the server before sending the result (which doesn't contain any server-only code) to the client. From the Client Component's perspective, its child will be already rendered.

In layouts, this pattern is applied with the children prop so you don't have to create an additional wrapper component.

For example, the ClientLayout component will accept the ServerPage component as its child:

app/dashboard/layout.js
// The Dashboard Layout is a Client Component
export default function ClientLayout({ children }) {
  // Can use useState / useEffect here
  return (
    <>
      <h1>Layout</h1>
      {children}
    </>
  );
}
 
// The Page is a Server Component that will be passed to Dashboard Layout
// app/dashboard/settings/page.js
export default function ServerPage() {
  return (
    <>
      <h1>Page</h1>
    </>
  );
}

Note: This style of composition is an important pattern for rendering Server Components inside Client Components. It sets the precedence of one pattern to learn, and is one of the reasons why we've decided to use the children prop.

Data fetching

It will be possible to fetch data inside multiple segments in a route. This is different from the pages directory, where data fetching was limited to the page-level.

Data fetching in Layouts

You can fetch data in a layout.js file by using the Next.js data fetching methods getStaticProps or getServerSideProps.

For example, a blog layout could use getStaticProps to fetch categories from a CMS, which can be used to populate a sidebar component:

app/blog/layout.js
export async function getStaticProps() {
  const categories = await getCategoriesFromCMS();
 
  return {
    props: { categories },
  };
}
 
export default function BlogLayout({ categories, children }) {
  return (
    <>
      <BlogSidebar categories={categories} />
      {children}
    </>
  );
}

Multiple data fetching methods in a route

You can also fetch data in multiple segments of a route. For example, a layout that fetches data can also wrap a page that fetches its own data.

Using the blog example above, a single post page can use getStaticProps and getStaticPaths to fetch post data from a CMS:

app/blog/[slug]/page.js
export async function getStaticPaths() {
  const posts = await getPostSlugsFromCMS();
 
  return {
    paths: posts.map((post) => ({
      params: { slug: post.slug },
    })),
  };
}
 
export async function getStaticProps({ params }) {
  const post = await getPostFromCMS(params.slug);
 
  return {
    props: { post },
  };
}
 
export default function BlogPostPage({ post }) {
  return <Post post={post} />;
}

Since both app/blog/layout.js and app/blog/[slug]/page.js use getStaticProps, Next.js will statically generate the whole /blog/[slug] route as React Server Components at build time - resulting in less client-side JavaScript and faster hydration.

Statically generated routes improve this further, as the client navigation reuses the cache (Server Components data) and doesn't recompute work, leading to less CPU time because you're rendering a snapshot of the Server Components.

Behavior and priority

Next.js Data Fetching Methods (getServerSideProps and getStaticProps) can only be used in Server Components in the app folder. Different data fetching methods in segments across a single route affect each other.

Using getServerSideProps in one segment will affect getStaticProps in other segments. Since a request already has to go to a server for the getServerSideProps segment, the server will also render any getStaticProps segments. It will reuse the props fetched at build time so the data will still be static, the rendering happens on-demand on every request with the props generated during next build.

Using getStaticProps with revalidate (ISR) in one segment will affect getStaticProps with revalidate in other segments. If there are two revalidate periods in one route, the shorter revalidation will take precedence.

Note: In the future, this may be optimized to allow for full data fetching granularity in a route.

Data fetching with React Server Components

The combination of Server-Side Routing, React Server Components, Suspense and Streaming have a few implications for data fetching and rendering in Next.js:

Parallel data fetching

Next.js will eagerly initiate data fetches in parallel to minimize waterfalls. For example, if data fetching was sequential, each nested segment in the route couldn't start fetching data until the previous segment was completed. With parallel fetching, however, each segment can eagerly start data fetching at the same time.

Since rendering may depend on Context, rendering for each segment will start once its data has been fetched and its parent has finished rendering.

In the future, with Suspense, rendering could also start immediately - even if the data is not completely loaded. If the data is read before it's available, Suspense will be triggered. React will start rendering Server Components optimistically, before the requests have completed, and will slot in the result as the requests resolve.

Partial Fetching and Rendering

When navigating between sibling route segments, Next.js will only fetch and render from that segment down. It will not need to re-fetch or re-render anything above. This means in a page that shares a layout, the layout will be preserved when a user navigates between sibling pages, and Next.js will only fetch and render from that segment down.

This is especially useful for React Server Components, as otherwise each navigation would cause the full page to re-render on the server instead of rendering only the changed part of the page on the server. This reduces the amount of data transfered and execution time, leading to improved performance.

For example, if the user navigates between the /analytics and the /settings pages, React will re-render the page segments but preserve the layouts:

Note: It will be possible to force a re-fetch of data higher up the tree. We are still discussing the details of how this will look and will update the RFC.

Route Groups

The hierarchy of the app folder maps directly to URL paths. But it’s possible to break out of this pattern by creating a route group. Route groups can be used to:

  • Organize routes without affecting the URL structure.
  • Opt a route segment out of a layout.
  • Create multiple root layouts by splitting the application.

Convention

A route group can be created by wrapping a folder’s name in parenthesis: (folderName)

Note: The naming of route groups are only for organizational purposes since they do not affect the URL path.

Example: Opting a route out of a layout

To opt a route out of a layout, create a new route group (e.g. (shop)) and move the routes that share the same layout into the group (e.g. account and cart). The routes outside of the group will not share the layout (e.g. checkout).

Before:

After:

Example: Organizing routes without affecting the URL path

Similarly, to organize routes, create a group to keep related routes together. The folders in parenthesis will be omitted from the URL (e.g. (marketing) or (shop)).

Example: Creating multiple root layouts

To create multiple root layouts, create two or more route groups at the top level of the app directory. This is useful for partitioning an application into sections that have a completely different UI or experience. The <html>, <body> and <head> tags of each root layout can be customized separately.

Server-Centric Routing

Currently, Next.js uses client-side routing. After the initial load and on subsequent navigation, a request is made to the server for the resources of the new page. This includes the JavaScript for every component (including components only shown under certain conditions) and their props (JSON data from getServerSideProps or getStaticProps). Once both the JavaScript and data are loaded from the server, React renders the components client-side.

In this new model, Next.js will use server-centric routing while maintaining client-side transitions. This aligns with Server Components which are evaluated on the server.

On navigation, data is fetched and React renders the components server-side. The output from the server are special instructions (not HTML or JSON) for React on the client to update the DOM. These instructions hold the result of the rendered Server Components meaning that no JavaScript for that component has to be loaded in the browser to render the result.

This is in contrast to the current default of Client components, which the component JavaScript to the browser to be rendered client-side.

Some benefits of server-centric routing with React Server Components include:

  • Routing uses the same request used for Server Components (no additional server requests are made).
  • Less work is done on the server because navigating between routes only fetches and renders the segments that change.
  • No additional JavaScript is loaded in the browser when client-side navigating when no new client components are used.
  • The router leverages a new streaming protocol so that rendering can start before all data is loaded.

As users navigate around an app, the router will store the result of the React Server Component payload in an in-memory client-side cache. The cache is split by route segments which allows invalidation at any level and ensures consistency across concurrent renders. This means that for certain cases, the cache of a previously fetched segment can be re-used.

Note

  • Static Generation and Server-side caching can be used to optimize data fetching.
  • The information above describes the behavior of subsequent navigations. The initial load is a different process involving Server Side Rendering to generate HTML.
  • While client-side routing has worked well for Next.js, it scales poorly when the number of potential routes is large because the client has to download a route map.
  • Overall, by using React Server Components, client-side navigation is faster because we load and render less components in the browser.

Instant Loading States

With server-side routing, navigation happens after data fetching and rendering so it’s important to show loading UI while the data is being fetched otherwise the application will seem unresponsive.

The new router will use Suspense for instant loading states and default skeletons. This means loading UI can be shown immediately while the content for the new segment loads. The new content is then swapped in once rendering on the server is complete.

While rendering is happening:

  • Navigation to the new route will be immediate.
  • Shared layouts will remain interactive while new route segments load.
  • Navigation will be interruptible - meaning the user can navigate between routes while the content of one route is loading.

Default loading skeletons

Suspense boundaries will be automatically handled behind-the-scenes with a new file convention called loading.js.

Example:

You will be able to create a default loading skeleton by adding a loading.js file inside a folder.

The loading.js should export a React component:

loading.js
export default function Loading() {
  return <YourSkeleton />
}
 
// layout.js
export default function Layout({children}) {
  return (
    <>
      <Sidebar />
      {children}
    </>
  )
}
 
// Output
<>
  <Sidebar />
  <Suspense fallback={<Loading />}>{children}</Suspense>
</>

This will cause all segments in the folder to be wrapped in a suspense boundary. The default skeleton will be used when the layout is first loaded and when navigating between sibling pages.

Error Handling

Error boundaries are React components that catch JavaScript errors anywhere in their child component tree.

Convention

You'll be able to create an Error Boundary that will catch errors within a subtree by adding a error.js file and default exporting a React Component.

The Component will be shown as a fallback if an error is thrown within that subtree. This component can be used to log errors, display useful information about the error, and functionality to attempt to recover from the error.

Due to the nested nature of segments and layouts, creating Error boundaries allows you to isolate errors to those parts of the UI. During an error, layouts above the boundary will remain interactive and their state will be preserved.

error.js
export default function Error({ error, reset }) {
  return (
    <>
      An error occurred: {error.message}
      <button onClick={() => reset()}>Try again</button>
    </>
  );
}
 
// layout.js
export default function Layout({children}) {
  return (
    <>
      <Sidebar />
      {children}
    </>
  )
}
 
// Output
<>
  <Sidebar />
  <ErrorBoundary fallback={<Error />}>{children}</ErrorBoundary>
</>

Note:

  • Errors inside a layout.js file in the same segment as an error.js will not be caught as the automatic error boundary wraps the children of a layout and not the layout itself.

Templates

Templates are similar to Layouts in that they wrap each child Layout or Page.

Unlike Layouts that persist across routes and maintain state, templates create a new instance for each of their children. This means that when a user navigates between route segments that share a template, a new instance of the component is mounted.

Note: We recommend using Layouts unless you have a specific reason to use a Template.

Convention

A template can be defined by exporting a default React component from a template.js file. The component should accept a children prop which will be populated with nested segments.

Example

template.js
export default function Template({ children }) {
  return <Container>{children}</Container>;
}

The rendered output of a route segment with a Layout and a Template will be as such:

<Layout>
  {/* Note that the template is given a unique key. */}
  <Template key={routeParam}>{children}</Template>
</Layout>

Behavior

There may be cases where you need to mount and unmount shared UI, and templates would be a more suitable option. For example:

  • Enter/exit animations using CSS or animation libraries
  • Features that rely on useEffect (e.g logging page views) and useState (e.g a per-page feedback form)
  • To change the default framework behavior. E.g. suspense boundaries inside Layouts only show the fallback the first time the Layout is loaded and not when switching pages. For templates, the fallback is shown on each navigation.

For example, consider the design of a nested layout with a bordered container that should be wrapped around every sub-page.

You could put the container inside the parent layout (shop/layout.js):

shop/layout.js
export default function Layout({ children }) {
  return <div className="container">{children}</div>;
}
 
// shop/page.js
export default function Page() {
  return <div>...</div>;
}
 
// shop/categories/layout.js
export default function CategoryLayout({ children }) {
  return <div>{children}</div>;
}

But any enter/exit animations wouldn’t play when switching pages because the shared parent layout doesn’t re-render.

You could put the container in every nested layout or page:

shop/layout.js
export default function Layout({ children }) {
  return <div>{children}</div>;
}
 
// shop/page.js
export default function Page() {
  return <div className="container">...</div>;
}
 
// shop/categories/layout.js
export default function CategoryLayout({ children }) {
  return <div className="container">{children}</div>;
}

But then you’d have to manually put it in every nested layout or page which can be tedious and error-prone in more complex apps.

With this convention, you can share templates across routes that create a new instance on navigation. This means DOM elements will be recreated, state will not be preserved, and effects will be re-synchronized.

Advanced Routing Patterns

We plan to introduce conventions to cover edge cases and allow you to implement more advanced routing patterns. Below are some examples we have been actively thinking about:

Intercepting Routes

Sometimes, it may be useful to intercept route segments from within other routes. On navigation, the URL will be updated as normal, but the intercepted segment will be shown within the current route’s layout.

Example

Before: Clicking the image leads to a new route with its own layout.

After: By intercepting the route, clicking the image now loads the segment within the current route’s layout. E.g. as a modal.

To intercept the /photo/[id] route from within /[username] segment, create a duplicate /photo/[id] folder inside the /[username] folder, and prefix it with the (..) convention.

Convention

  • (..) - will match the route segment one level higher (sibling of the parent directory). Similar to ../ in relative paths.
  • (..)(..) - will match the route segment two levels higher. Similar to ../../ in relative paths.
  • (...) - will match the route segment in the root directory.

Note: Refreshing or sharing the page would load the route with its default layout.

Dynamic Parallel Routes

Sometimes it may be useful to show two or more leaf segments (page.js) in the same view that can be navigated independently.

Take for instance two or more tab groups within the same dashboard. Navigating one tab group should not affect the other. The combinations of tabs should also be correctly restored when navigating backwards and forwards.

Convention

By default, layouts accept a prop called children which will contain a nested layout or a page. You can rename the prop by creating a named "slot" (a folder that includes the @ prefix) and nesting segments inside it.

After this change, the layout will receive a prop called customProp instead of children.

analytics/layout.js
export default function Layout({ customProp }) {
  return <>{customProp}</>;
}

You can create parallel routes by adding more than one named slot at the same level. In the example below, both @views and @audience are passed as props to the analytics layout.

You can use the named slots to display leaf segments simultaneously.

analytics/layout.js
export default function Layout({ views, audience }) {
  return (
    <>
      <div>
        <ViewsNav />
        {views}
      </div>
      <div>
        <AudienceNav />
        {audience}
      </div>
    </>
  );
}

When the user first navigates to /analytics, the page.js segment in each folder (@views and @audience) are shown.

On navigation to /analytics/subscribers, only @audience is updated. Similarly, only @views are updated when navigating to /analytics/impressions.

Navigating backwards and forwards will reinstate the correct combination of parallel routes.

Combining Intercepting and Parallel Routes

You can combine intercepting and parallel routes to achieve specific routing behaviors in your application.

Example

For example, when creating a modal, you often need to be aware of some common challenges, such as:

  • Modals not being accessible through an URL.
  • Modals closing when the page is refreshed.
  • Backwards navigation going to the previous route rather than the route behind the modal.
  • Forwards navigation not reopening the modal.

You may want the modal to update the URL when it opens, and backwards/fowards navigation to open and close the modal. Additionally, when sharing the URL, you may want the page to load with the modal open and context behind it or you may want the page to load the content without the modal.

A good example of this are photos on social media sites. Usually, photos are accessible within a modal from the user's feed or profile. But when sharing the photo, they shown directly on their own page.

By using conventions, we can make the modal behavior map to routing behavior by default.

Consider this folder structure:

With this pattern:

  • The content of /photo/[id] is accessible through an URL within its own context. It's also accessible within a modal from within the /[username] route.
  • Navigating backwards and forwards using client-side navigation should close and reopen the modal.
  • Refreshing the page (server-side navigation) should take the user to the original /photo/id route instead of showing the modal.

In /@modal/(..)photo/[id]/page.js, you can return the content of the page wrapped in a modal component.

/@modal/(..)photo/[id]/page.js
export default function PhotoPage() {
  const router = useRouter();
 
  return (
    <Modal
      // the modal should always be shown on page load
      isOpen={true}
      // closing the modal should take user back to the previous page
      onClose={() => router.back()}
    >
      {/* Page Content */}
    </Modal>
  );
}

Note: This solution isn't the only way to create a modal in Next.js, but aims to show how you can combine conventions to achieve more complex routing behavior.

Conditional Routes

Sometimes, you may need dynamic information like data or context to determine what route to show. You can use parallel routes to conditionally load one route or another.

Example

layout.js
export async function getServerSideProps({ params }) {
  const { accountType } = await fetchAccount(params.slug);
  return { props: { isUser: accountType === 'user' } };
}
 
export default function UserOrTeamLayout({ isUser, user, team }) {
  return <>{isUser ? user : team}</>;
}

In the example above, you can return either the user or team route depending on the slug. This allows you to conditionally load the data and match the sub-routes against one option or the other.

Conclusion

We're excited about the future of layouts, routing, and React 18 in Next.js. Implementation work has begun and we'll announce the features once they are available.

Leave comments and join the conversation on GitHub Discussions.