[Frontend Note]Does NextJS App Routing Have Its Quirks?

NextJS App Routing Cheatsheet

Posted by Jamie on Friday, April 5, 2024

Cheatsheet

  • app/folderA/folderB/page.js. App routing, where the router is {domain}/folderA/folderB.
  • app/folderA/[id]/page.js. Dynamic routing, matching any {domain}/folderA/{any id}.
  • app/folderA/[...id]. A catch-all for sub-routes, capturing any level of routes, equating to matches like {domain}/folderA/aaa/bbb/ccc. However, accessing {domain}/folderA results in a 404.
  • app/folderA/[[...id]] . Besides catch-all for sub-routes, it includes that level’s route (domain/folderA), though the params will be empty.
  • (folderA) . Simply adding a folderA directory, not included in the route. // TODO:
  • @name. Parallel routes, allowing layout.tsx to render multiple page.tsx.
  • (.)name. Intercepting routes, presenting different displays between router and direct access. The brackets denote a relative path.

Make Some Examples

Basic App Routes

Under the ./app/... directory, each directory acts as a sub-route, enabling the direct creation of nested routes using directories and subdirectories.

Dynamic Routes-general

In many real-world scenarios, routes are dynamicaly generated, such as bolg post IDs. For accessing a blog post via https://{domain}/blog/{blogID} , the NextJS App Routing would be:

.
└── app
    └── blog
        └── [blogID]
            └── page.tsx

Dynamic Routes - catch all

The catch all proivides tremendous flexibility for dynamic routes. For instance, on an e-commerce site, users could access {domain}/products/electronics/phones or even deeper {domain}/products/electronics/phones/samsung with a single component by using catch all to capture and dynamically display based on the route. As per the example, if the app routing is:

.
└── app
    └── products
        └── [...data]
            └── page.tsx

Then, the params for /products/electronics/phones would be:

{ data: ["electronics", "phones"] }

And for products/electronics/phones/samsung:

{ data: ["electronics", "phones", "samsung"] }

However, directly entering {demain}/products in the browser would result in a 404.

Dynamic Routes - optional catch all

Optional Catch-all in dynamic routes aims to address this issue. Modifying the directory to:

.
└── app
    └── products
        └── [[...data]]
            └── page.tsx

Now allows direct access to {demain}/products, although the params would be an empty object, {} .

Routes Group

This is handy for organizing project folders. Imagine a dashboard project requiring login, whrere sign-in and sign-up share one layout, and dashboard, services, settings use another one, all without needing new main routes(all are top-level routes). The directory structure would ideally be:

.
└── app
    ├── dashboard
    ├── services
    ├── settings
    ├── sign-in
    └── sign-up

However, adding layout according to requirements would introduce a lot of redundant code:

.
└── app
    ├── dashboard
    │   ├── layout.tsx
    │   └── page.tsx
    ├── services
    │   ├── layout.tsx
    │   └── page.tsx
    ├── settings
    │   ├── layout.tsx
    │   └── page.tsx
    ├── sign-in
    │   ├── layout.tsx
    │   └── page.tsx
    └── sign-up
        ├── layout.tsx
        └── page.tsx

In such scenarios, implying the Routes Group method greatly improves readability and reduces duplicate layout.tsx.

.
└── app
    ├── (auth)
    │   ├── layout.tsx
    │   ├── sign-in
    │   │   └── page.tsx
    │   └── sign-up
    │       └── page.tsx
    └── (main)
        ├── dashboard
        │   └── page.tsx
        ├── layout.tsx
        ├── services
        │   └── page.tsx
        └── settings
            └── page.tsx

Parallel Routes

Parallel routing feels like Vue’s <slot/> to me, allowing retrieval of @folderName defined in the directory within the same level layout.tsx (the nearst parent layout.tsx). For example, wiht the directory structure:

.
└── app
    ├── @auth
    │   └── page.tsx
    ├── @user
    │   └── page.tsx
    ├── layout.tsx
    └── page.tsx

And then we could capture these named slots in layout.tsx:

export default function Layout({
  children,
  auth,
  user,
}: {
  children: ReactNode
  auth: ReactNode
  user: ReactNode
}) {
  return (
    <>
      {children}
      {auth}
      {user}
    </>
  )
}

This setup allows auth and user to excute their own stream SSR if they have their loading.tsx. Also, conditioal rendering could be applied in layout.tsx (assuming different layouts for auth and user):

export default function Layout({
  children,
  auth,
  user,
}: {
  children: ReactNode
  auth: ReactNode
  user: ReactNode
}) {
  return (
    <>
      {children}
      { condition? auth : user}
    </>
  )
}

However, only becoming familiar with NextJS’s approach could make this more convenient; otherwise, direct <Suspense/> and conditional rendering could suffice. For instance, changing the directory to:

.
└── app
    ├── _components
    │   ├── auth-loding.tsx
    │   ├── auth.tsx
    │   ├── user-loading.tsx
    │   ├── user.tsx
    │   └── index.tsx
    ├── layout.tsx
    └── page.tsx

And then manually writing <Suspense/> in layout.tsx:

import { Suspense } from 'react'
import { Auth, AuthLoading, User, UserLoading } from './_components'
 
export default function Layout({children}:{children:ReactNode}) {
  return (
    <>
      {children}
      <Suspense fallback={<AuthLoading/>}>
        <Auth />
      </Suspense>
      <Suspense fallback={<UserLoading/>}>
        <User />
      </Suspense>
    </>
  )
}

Would also be a feasible approach, not to mention pure React of conditional rendering. This part might warrant further exploration.

Intercepting Routes

I haven’t yet had the chance to use intercepting routes, so let’s just demo it quickly:

  • Directory structure:

    .
    └── app
    └── folderA
        ├── (.)folderB
        │   └── page.tsx
        ├── folderB
        │   └── page.tsx
        └── page.tsx
    
  • folderA/page.tsx:

    "use client";
    import { useRouter } from "next/navigation";
    
    export default function Page() {
    const router = useRouter();
    const goToFolderB = () => {
    router.push("folderA/folderB");
    };
    return (
    <div>
      <h1>FolderA</h1>
      <button onClick={goToFolderB}>route to folderB</button>
    </div>
    );
    }
    
  • folderA/folderB/page.tsx

    export default function Page() {
    return <h1>Original FolderB</h1>;
    }
    
  • folderA/(.)folderB/page.tsx

    export default function Page() {
    return <h1>(.) Intercepted FolderB</h1>;
    }
    

Ine the demo project above, if one navigates from {domain}/folderA to {domain}/folderB via button click, the content displayed will be (.) Intercepted FolderB. However, if entering {domain}/folderA/folderB directly into the browser, the display will be Original FolderB, indicating that a context-aware scenario (like roue transitions), the route will be intercepted and render (.)folder/page.tsx.

Conclusion

Getting used to NextJS App Routing might take some time initially (especially if trying to apply one’s own logic, which could lead to some code redundancy), but once accustomed, you’ll find NextJS truly optimizes for engineer usability, front-end scenarios, and more. Looking forward to creating more exciting projects with NextJS :)

Ref

ChangeLog

  • 20240405-初稿