Why is Next.js's routing so odd?

nextjser

Nextjser

Posted on September 4, 2024

Why is Next.js's routing so odd?

Next.js is a full-stack framework for React, primarily focused on server-side rendering (SSR). It has a very powerful, yet somewhat unusual, routing mechanism. What does this routing mechanism look like? And why is it considered strange? Let's try it out and see. First, let's create a Next.js project:

npx create-next-app@latest
Enter fullscreen mode Exit fullscreen mode

Image description

Run create-next-app and enter some basic information, and your Next.js project will be set up.

Next, navigate into the project directory and run npm run dev to start the development server:

Image description

If you can see the page in your browser, that means it’s running successfully:

Image description

In the project directory, you'll find an app folder under src, which contains a page.tsx file:

Image description

Let's add a few directories:

Image description

/nextjser/handsome/page.tsx

export default function Handsome() {

return <div>handsome</div>

}
Enter fullscreen mode Exit fullscreen mode

/nextjser/nice/page.tsx

export default function Nice() {

return <div>nice</div>

}
Enter fullscreen mode Exit fullscreen mode

Then visit the following link in browser:

Image description

Image description

You can see that by adding a few directories, several corresponding routes are automatically created.

This is Next.js's file system-based routing.

Having just learned that page.tsx is used to define pages, what if multiple pages have common parts?

For example, what about menus and navigation like this:

Image description

Definitely not one copy per page.

This kind of definition goes inside layout.tsx.

app/layout.tsx is for defining the outermost layout:

That is, the HTML structure, as well as information like title and description:

Image description

You can see these in the HTML source code of the web page:

Image description

Image description

Not only can the root route define a layout, but every level can as well:

Image description

export default function Layout({
    children,
}: Readonly<{
    children: React.ReactNode;
}>) {

return (
    <div>
        <div>Left menu</div>    
        {children}
    </div>
);

}
Enter fullscreen mode Exit fullscreen mode

Next.js will automatically wrap the page.tsx components with the layout.tsx components on the outer layer.

Image description

Some friends might notice a gradient background, which is defined in global.css. Let's remove it:

Image description

Image description

Then continue to look at:

We can use the Link component to navigate between different routes:

Image description

Image description

Dynamic Routes

Some friends might say, "This is all quite normal."

So let's look at something not so normal next:

If I want to define a page for a route like /dong/111/xxx/222 (where 111 and 222 are parameters in the path), how should I write it?

You could like this:

Image description

interface Params {
    params: {
        param1: string;
        param2: string;
    }
}
export default function Xxx(
    { params }: Params
) {
    return <div>
        <div>xxx</div>
        <div>params:{JSON.stringify(params)}</div>
    </div>
}

Enter fullscreen mode Exit fullscreen mode

Parameters in the path are named using the [xxx] notation.

Next.js will extract the parameters from the path and pass them into the component:

Image description

This is called a dynamic route.

Catch-all Segments

What if you want both /dong2/a/b/c and /dong2/a/d/e to render the same component?

You can code it like this:

Image description

interface Params {
    params: {
        dong: string;
    }
}
export default function Dong2(
    { params }: Params
) {
    return <div>
        <div>dong2</div>
        <div>params:{JSON.stringify(params)}</div>
    </div>
}
Enter fullscreen mode Exit fullscreen mode

The syntax [...slug] is used to define routes with arbitrary depth, known as catch-all routes.

Image description

Image description

You can see that any route under /dong2 will render this component.

Optional Catch-all Segments

What if I directly access /dong2?

Image description

You can see that it results in a 404 error.

But this can also be supported; just add another set of brackets to make it [[...slug]], and it will work:

Image description

With this change, /dong2 will also render this component, although the parameters will be empty.

Image description

This type of route, [[...dong]], is called an optional catch-all.

Route Groups

You can see that the directories in a Next.js project are not just simple directories; they have corresponding routing implications.

But what if I just want to add a plain directory that is not included in the routing?

You can write it like this:

Image description

I added a directory called "(dongGroup)" outside of both dong and dong2; will the previous routes change?

Image description

I tried it, and it remains unchanged.

This means that as long as you add a () around the directory name, it won't be included in the routing; it's just for grouping purposes, which is called a routing group.

Now, we have rendered a single page under one layout.

Parallel Routes

But what if I want one layout to render multiple pages?

You can write it like this:

Image description

Under the parallel directory, there are 3 pages: page.tsx, @aaa/page.tsx, and @bbb/page.tsx.

They will be passed into layout.tsx with the parameters children, aaa, and bbb, respectively.

layouts

export default function Layout({
  children,
  aaa,
  bbb
}: {
  children: React.ReactNode,
  aaa: React.ReactNode,
  bbb: React.ReactNode
}) {
  return (
    <div>
        <div>{children}</div>
        <div>{aaa}</div>
        <div>{bbb}</div>
    </div>
  )
}

Enter fullscreen mode Exit fullscreen mode

page.tsx

export default function Page() {
    return <div>page</div>
}

Enter fullscreen mode Exit fullscreen mode

@aaa/page.tsx

export default function Aaa() {
    return <div>aaa</div>
}

Enter fullscreen mode Exit fullscreen mode

@bbb/page.tsx

export default function Bbb() {
    return <div>bbb</div>
}

Enter fullscreen mode Exit fullscreen mode

The rendering will look like this:

Image description

You can see that in the layout, the contents of 3 pages are included, all rendered out. This is called parallel routing.

Some friends might ask, can I access /parallel/@aaa?

Image description

No, it is not possible.

Additionally, Next.js has a very powerful routing mechanism:

Intercepting Routes

There was such a route before:

Image description

We define a route at the same level as it:

Image description

import Link from "next/link";

export default function Go() {
    return <div>
        <div>
            <Link href="/nextjser/liu">to nice</Link>
        </div>
        <div>go</div>
    </div>
}
Enter fullscreen mode Exit fullscreen mode

Clicking the link will take you to http://localhost:3000/nextjser/nice

Image description

There's no problem with that.

But what if I add a directory named (..)nice under go:

Image description

At this point, try it again:

Image description

You can see that this time the rendered nice component has been replaced, but if you refresh, it's still the previous component.

Many people might wonder, what's the use of this?

A scenario example will make it clear.

For instance, with a table, when you click on each item, an edit dialog pops up. This edit page can be shared, and when the shared link is opened, it shows the complete edit page.

Image description

That is to say, in different scenarios, you can override the component rendered by this URL, which is the use of route interception.

The usage is also very simple. Since you want to intercept the /nextjser/nice route at the upper level, you need to add (..) in front.

Image description

Similarly, (.)xx represents intercepting the route of the current directory, (..)(..)xx intercepts the route of the parent's parent directory, and (...)xxx intercepts the root route.

This kind of route interception is very useful in specific scenarios.

These are the routing mechanisms related to pages, which are quite powerful, aren't they?

APIs

Of course, these routing mechanisms are not only for pages; Next.js can also be used to define APIs such as Get and Post.

Just replace page.tsx with route.ts:

Image description

import { NextResponse, type NextRequest } from 'next/server';

const data: Record<string, any> = {
    1: {
        name: 'guang',
        age: 20
    },
    2: {
        name: 'dong',
        age: 25
    }
}

export async function GET(request: NextRequest) {
    const { searchParams } = new URL(request.url);
    const id = searchParams.get('id');

    return NextResponse.json(!id ? null : data[id]);
}
Enter fullscreen mode Exit fullscreen mode

We have defined the GET method for the /guang3 route, which retrieves data based on the id.

Let's visit it:

Image description

Image description

The routing concepts we learned earlier can all be applied to route.ts.

For example:

Image description

[id] defines a dynamic route parameter, and [...yyy] matches arbitrary routes.

In the GET method of route.ts, you also retrieve them through params:

import { NextResponse, type NextRequest } from 'next/server';

interface Params {
    params: {
        id: string;
        yyy: string;
    }
}

export async function GET(request: NextRequest, { params }: Params) {
    return NextResponse.json({
        id: params.id,
        yyy: params.yyy
    });
}
Enter fullscreen mode Exit fullscreen mode

Image description

App router & Page router

Do you feel why Next.js is called a full-stack framework rather than just an SSR (Server-Side Rendering) framework?

This is because, in addition to rendering React components, it can also define APIs.

In this way, we have gone through the routing mechanism of Next.js.

This kind of routing mechanism is called the app router, which means the top level is the app directory:

Image description

Previously, there was also a page router, where the top-level directory was pages.

These two are just two different file and directory naming conventions; we only need to learn app router, as it is the latest routing mechanism.

Summary

Let's summarize what we've learned:

  • aaa/bbb/page.tsx can define the route for /aaa/bbb.
  • aaa/[id]/bbb/[id2]/page.tsx contains [id] as dynamic route parameters, which can be accessed within the component.
  • aaa/[...xxx]/page.tsx can match any route like /aaa/xxx/xxx/xxx, called a catch-all dynamic route. However, it does not match /aaa.
  • aaa/[[...xxx]]/page.tsx is similar to the above but matches /aaa as well, called an optional catch-all dynamic route.
  • aaa/(xxx)/bbb/page.tsx where (xxx) is just for grouping and does not participate in routing, called a routing group.
  • aaa/@xxx/page.tsx can be included multiple times in layout.tsx, called parallel routing.
  • aaa/(..)/bbb/page.js can intercept the /bbb route, rewrite the corresponding component, but after refreshing, it still renders the original component, called an intercepting route.

These routing mechanisms may indeed seem quite peculiar, and they can make a Next.js project look like this:

Image description

Compared to this file system-based routing, many might be more familiar with the programmatic routing style of React Router:

Image description

Next.js's declarative routing is actually quite convenient once you get used to it.

There's no need to maintain separate routing logic; the directory structure itself defines the routes, making it clear at a glance.

Moreover, these seemingly strange syntaxes actually make sense when you think about them:

  • For example, [xxx] is a common syntax for matching parameters in a URL.

  • [...xxx] simply adds ... to it, which in JavaScript signifies an arbitrary number of arguments, so it's used to match routes with arbitrary segments.

  • Adding another set of brackets [[...xxx]] indicates that the route can be optional, which is also a natural design choice.

  • (.)xx and (..)xxx use . and .., which are symbols in file systems, making them quite natural for intercepting routes.

  • Routing groups are denoted by parentheses (xxx) to indicate grouping, and parallel routes are indicated by @ in @xxx to show that multiple pages can be included, all of which are intuitive designs.

So, Next.js's implementation of this routing mechanism based on the file system, with these seemingly strange syntaxes, are actually quite reasonable designs.

We've learned about Next.js's routing mechanism, which is defined based on the file system for interfaces or page routes.

Next.js's routing mechanism is quite powerful, supporting many features, These syntaxes may seem a bit odd at first glance, but upon closer consideration, they are quite reasonable designs.

💖 💪 🙅 🚩
nextjser
Nextjser

Posted on September 4, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related