🔖

Next.js App Router (app ディレクトリ) の逆引き辞典

2023/04/16に公開
1

Next.js v13 から App Router 機能 (app ディレクトリ) が新しく追加されました。 (v13.3.0 現在はベータ版です。 v13.4.0 をもって安定版になりました!)

  • ファイルベースの Layout 機能
  • 処理の一部を Server Component に移しバンドルサイズを削減できる
    • 例: remark を利用した Markdown のパース

が有名なところだと思いますが、アーキテクチャの大幅な変更のおかげで、 開発者とユーザー両者の体験を改善できるっぽいです。


ドキュメントが英語で書かれていて、かつ把握し切れないほど膨大なので、

pages で使えたあの機能って、App Router ではどう書けばいいの?

という初歩的な疑問点や、ドキュメント未記載の機能に着目して、逆引きっぽい形式でざっくりとまとめてみました。

割愛した要素

以下の要素については、煩雑になる or ドキュメントだけで理解しやすそうなので、割愛しています。

▼ Deduping についてはこちらの記事をご覧ください

https://zenn.dev/cybozu_frontend/articles/next-caching-dedupe

前提: Routing Fundamentals

https://nextjs.org/docs/app/building-your-application/routing

Routing Fundamentals にはかなり情報がまとまっているので、まずはそちらに目を通しましょう。

特に、上記の記事に登場する用語・ファイル名規約の一部は、この記事を読むにあたっての前提知識とします。

Server / Client Component

Next.js App Router の目新しい点といえば、 React Server Components が導入されたことだと思います。

https://nextjs.org/docs/getting-started/react-essentials

「Server Component は制約がキツく、扱いづらい」と感じる人が多いと思うので、当記事ではそこにだけ絞り込んで解説します。

SSR の認識について注意 (2023/04/16 追記)

Client Component も SSR (Server Side Rendering) および SG (Static Generation) の対象になります。

このように、 Server / Client Component の境界線と SSR・SG / CSR (Client Side Rendering) の境界線は完全に一致するわけではないので、そこを認識しておく必要があると思います。

もちろん、 Server Component はクライアント側にレンダリング結果だけを返して蒸発するので、 サーバー側(あるいはビルドする端末) 上でしか利用できません。

https://nextjs.org/docs/app/building-your-application/rendering#static-and-dynamic-rendering-on-the-server

Client Component は末端へ

上記の記事には 「we recommend moving Client Components to the leaves」 (Client Component を末端に寄せるのを推奨します) とあります。

次の節にあるように、 Client Component (以下 Client Comp.) から Server Component を利用することができないので、 コンポーネントツリー構造のルート(Root のほう) の側に Client Comp. を置いてしまうと、 Server Comp. を配置できる場所が限られてしまいます。

そうなると、ページの構成などによっては、 Server Comp. 特有のユーザー/開発者体験を享受する機会が失われてしまうことになります。

なので、できる限り Client Comp. の使用箇所をツリーの末端に寄せて、ルートに近いところは Server Comp. で固めるのがベストだと思います。

どうしてもツリーの構築が上手くいかない場合は、 Composition (children Prop) を使えば解決するかもしれません。

🚫 Client → Server はダメ

Next.js の App Router では、 Client Component から Server Component を利用することが出来ないようになっています。

SomeClientComponent.tsx
"use client"

function SomeClientComponent() {
  // 🚫 DON'T: Client -> Server は利用できない 
  return <SomeServerComponent />
}

✅ children を使う

そこで children prop (composition パターンとも呼ぶ) を活用すると、 Client Component の中に Server Component を配置することができます。

https://react.dev/learn/passing-props-to-a-component#passing-jsx-as-children

というより、Next.js でまともに Server Component の恩恵を受けるには composition が必須です。高等テクニックと思って避けずに、ガンガン使いましょう!

SomeClientComponent.tsx
"use client"

type Props = { children: ReactNode }

function SomeClientComponent({ children }: Props) {
  return <div>{children}</div>
}
ParentServerComponent.tsx
function ParentServerComponent() {
  return (
    <SomeClientComponent>
      {/* ✅ DO: children としてなら渡せる */}
      <SomeServerComponent>
    </SomeClientComponent>
  );
}

SG 静的生成: output: 'export'

Next.js 13.2.x 以前では、 next export コマンドを利用して SG (静的生成) 機能を利用していましたが、 Next.js 13.3.0 からは、 next.config.jsoutput オプションで指定することになります。

App Router だと、 next dev で開発用サーバーを立ち上げて、対象のページを表示した時点で SG 不可能になる機能 の使用を検知して、エラーを表示してくれます。

新しく入った Server Component という名前の第一印象で、 サーバーが必要であるかのように思ってしまいますが、それとは裏腹に、 App Router は以前よりも SG フレンドリーになったと 思います。

ちなみに、 Server Component と SG を組み合わせると 「ビルド時にレンダリングされ、JS のバンドルを肥大させないコンポーネント」 が作れます。 Jamstack で Markdown をパースするようなケースでは恐らく最強です。

https://nextjs.org/docs/app/building-your-application/deploying/static-exports

next.config.js
/**
 * @type {import('next').NextConfig}
 */
const nextConfig = {
  output: 'export',
}

module.exports = nextConfig

Static / Dynamic Rendering

Next.js の従来の page ディレクトリでは、 (動的な) SSR がデフォルトで SG (静的生成) の機能が後付けされました。

Automatic Static Optimization という機能があることからも分かるように、あくまで「動的な要素がないときに、ビルド時に事前にHTMLを書き出してくれる」というものでした。

App Router では、うって変わって Static Rendering (静的レンダリング) がデフォルトになります。

Static / Dynamic はルート単位

Static / Dynamic Rendering はルート単位で決定されます。 (将来は更にレイアウト・ページも別々に設定できるようになるようです。)

Static Rendering がデフォルト

App Router では、 Static Rendering がデフォルトになり、ビルド時に確定する内容はビルド時に書き出されます。

注意が必要なのは、「動的なつもりの記述が静的になってしまう」ケースがあることです。

Server Component で fetch() でデータを GET したり、 DB 等からデータ取得する場合には、デフォルトだと 「ビルド時に取得したっきりで、古い結果だけが表示されつづける」 ことになります。

リクエストのたびにデータを取得して欲しい場合は、 Dynamic Rendering (動的レンダリング) を有効化 する必要があります。

Dynamic Rendering の有効化

Dynamic Functions(動的な機能) または Dynamic Data Fetching (動的なデータ取得)を使ったときに、初めて Dynamic Rendering (動的レンダリング) が有効化されます。 (オプトイン)

https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic-rendering

Dynamic Functions (動的な機能)

  • Server Component 内で使える cookies(), headers()
    • このルート内全体がリクエスト時にレンダリングされる
  • Client Component 内で使える useSearchParams()
  • page.tsxsearchParams Prop を利用する

Dynamic Data Fetching (動的なデータ取得)

fetch()を使った場合fetch() はデフォルトで Static Data Fetching (静的データ取得) として認識されるので cache: 'no-store' オプションを渡すことで、Dynamic Data Fetching として認識させることができます。

Segment Config (セグメント単位の設定) として定数をエクスポートすることによっても制御できます。

fetch 以外によるデータ取得 (例えば、DB からの取得クエリの実行など) では、こちらの方法が必要になります。

app/now/page.tsx
// ⚠ これを設定しなかった場合、ずっとビルドを走らせた時刻が表示される。
export const dynamic = "force-dynamic";

const format = new Intl.DateTimeFormat("ja-JP", {
  timeZone: "Asia/Tokyo",
  timeStyle: "full",
});

export default function Page() {
  // 現在時刻の取得 (fetch 以外のデータ取得の例)
  const now = new Date(); 
  
  return (
    <div>
      <div>Now: </div>
      <div>{format.format(now)}</div>
    </div>
  );
}

https://nextjs.org/docs/app/building-your-application/data-fetching/fetching#dynamic-data-fetching

https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic

Dynamic Segment はどうなる?

ごめんなさい、わかりません。

Dynamic Segment (params, useParams) が Static / Dynamic Rendering
にどう関わるのかについての明示的な記述がないので、何とも言えません。

generateStaticParams, dynamicParams などが絡むと Static になったり Dynamic になったりするので、公式は明言を避けているのでしょうか。

https://nextjs.org/docs/app/api-reference/functions/generate-static-params

isReady は、もう要らない

Next.js で開発していてイライラするポイント No.1 が 「クエリパラメータや動的パスの取得が煩雑になる」 ことでした。(個人調べ)

App Router では、アーキテクチャの変更によって極めてシンプルになっています。 「isReady を使って useEffect 内で条件分岐する」のはもう不要になりました。

Route Groups: app/(secure)

ディレクトリ名を () で囲うと Route Groups の機能が使用できます。

裏側ではファイルツリーと同く (secure) というセグメントがあるかのように振る舞いますが、実際のアプリケーションのパスには現れません。

ファイルパス 実際のパス
app/(non-secure)/page.tsx /
app/(non-secure)/privacy-policy/page.tsx /privacy-policy
app/(secure)/admin/page.tsx /admin/
app/(secure)/users/hoge/page.tsx /users/hoge

例えば、

  • / と それ以外のパスでレイアウトを別々にするとき
  • 一つのセグメント以下で、レイアウトや共通処理を別々にするとき
    • /admin/**, /users/** はログインが必要、 それ以外のパスは全てログイン不要、のように

のようなケースで使用できます。

https://nextjs.org/docs/app/building-your-application/routing/defining-routes#route-groups

ルーティング対象外: app/_components/

Next.js App Router は _ から始まる名前のフォルダを、ルーティング制御から除外してくれます。

ルートに基づいてコードをまとめて配置 (Colocation) する都合上、複数のルートから呼び出されるコードの置き場所に困るかもしれません。

そのようなコードの置き場として、ルーティング対象外になる _something/ のようなディレクトリを利用することができます。

  • app/_utils/dete-time-format.ts
    • 日付フォーマットの関数を export
  • app/_components/Button.ts
    • 汎用ボタンコンポーネント

https://nextjs.org/blog/next-13-3#other-improvements

Mutating データ更新 router.refresh()

Server Component 上でデータを更新する方法については、 まだ仕様策定中(?) です。

ワークアラウンド (とりあえずの方法) としては、 router.refresh() を使うことで実現できます。

https://beta.nextjs.org/docs/data-fetching/mutating

Server Actions (Alpha)

データを更新するアクションについては、 Server Actions 機能として切り出されました。 この記事での解説の対象外とします。とりあえず公式のドキュメントを参照してください。

https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions

useRouter の大幅変更

App Router の useRouter は、 next/router ではなく next/navigation からインポートできます。

pathname, query については、このあとの章で解説するので読み進めてください。(もしくは目次からジャンプ)

router.events は今のところサポート外です。(これは地味に痛い) useEffect で解決する場合はそちらを使いましょう。

https://nextjs.org/docs/app/api-reference/functions/use-router

Dynamic Segments 動的セグメント: /posts/[slug]

https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes

ファイル名: /posts/[userId]/[slug]
実際のパス: /posts/honey32/next-13-app-overview

から { userId: "honey32", slug: "next-13-app-overview" } を取り出したいとき、

従来の page ルートでは、この動的セグメントとクエリパラメータが同じ router.query オブジェクトに押し込まれていましたが、 App Router では扱い方が大きく異なります。

router.query の扱いが非常に難しかったことを考えると、 App Router ではキチンと分離されて、Web 標準の機能だけで出来るようになって分かりやすくなったと思います。

href や push() で指定する方法

query オブジェクトを渡す、という方法は App Router では使えません。 自力で文字列を構築しましょう。

<Link href={`/posts/${post.userId}/${post.slug}`}>
  ここに記事のタイトルを書く
</Link>

実験的機能の Statically Typed Links を用いれば、 TypeScript によってパス文字列に型チェックを掛けることができます。

https://nextjs.org/docs/app/building-your-application/configuring/typescript#statically-typed-links

この記事は逆引きで公式ドキュメントに誘導するだけにして、詳細は省きます。

layout.tsx → params prop

https://nextjs.org/docs/app/api-reference/file-conventions/layout#params-optional

page.tsx → params prop

https://nextjs.org/docs/app/api-reference/file-conventions/page#params-optional

useParams() hook

この Hook は、 Client Component であれば Layout / Page どちらからでも利用できます。

https://nextjs.org/docs/app/api-reference/functions/use-params

SearchParams クエリパラメータ: ?limit=15

Dynamic Segments と同じく、クエリパラメータを扱う方法も大きく変わりました。

従来の page ルートでは、この動的セグメントとクエリパラメータが同じ router.query オブジェクトに押し込まれていましたが、 App Router では扱い方が大きく異なります。

href や push() で指定する方法

query オブジェクトを渡す、という方法は App Router では使えません。 URLSearchParams を活用して文字列を構築しましょう。

コンフィグを設定すると、型チェックが効くようになります。

const params = new URLSearchParams([
  ["user", "honey32"],
]);

<Link href={`/search?${params.toString()}`}>
  リンクテキスト
</Link>

layout.tsx では読み取れない

⚠ Layout においては Search Params を読み取ることができません。

  • searchParams prop もありません
  • Layout コンポーネントを Client Component にして useSearchParams() を使用できません。
    • Hydration Error が発生します。
    • Layout コンポーネントが利用するコンポーネント内でも同様に使用不可です。

page.tsx → searchParams prop

オブジェクト ({ key: string | string[] })として受け取れます。

https://nextjs.org/docs/app/api-reference/file-conventions/page#searchparams-optional

useSearchParams() hook

この Hook は Client Component から利用できます。

ただし、 Layout および Layout から利用されるコンポーネントでは使用できません。 Hydration Error が発生します。

イミュータブルな URLSearchParams オブジェクトが返ります。

https://nextjs.org/docs/app/api-reference/functions/use-search-params

レイアウトにおいて相対的にパスを取得

レイアウトコンポーネントまたはそれに利用されるコンポーネントで以下の Hooks を使用して、 レイアウトの子のパスのうち、どれが選択されているか を取得することができます。

useSelectedLayoutSegment() -> string | null

公式ドキュメントより引用

Layout Visited URL Returned Segment
app/layout.js / null
app/layout.js /dashboard 'dashboard'
app/dashboard/layout.js /dashboard null
app/dashboard/layout.js /dashboard/settings 'settings'
app/dashboard/layout.js /dashboard/analytics 'analytics'
app/dashboard/layout.js /dashboard/analytics/monthly 'analytics'

https://nextjs.org/docs/app/api-reference/functions/use-selected-layout-segment

useSelectedLayoutSegments() -> string[]

ドキュメントより引用

Layout Visited URL Returned Segments
app/layout.js / []
app/layout.js /dashboard ['dashboard']
app/layout.js /dashboard/settings ['dashboard', 'settings']
app/dashboard/layout.js /dashboard []
app/dashboard/layout.js /dashboard/settings ['settings']

https://nextjs.org/docs/app/api-reference/functions/use-selected-layout-segments

パスを取得(クエリパラメータは含まない)

usePathname()

この Hook は、 Layout においても Page においても利用できます。

https://nextjs.org/docs/app/api-reference/functions/use-pathname

株式会社ゆめみ

Discussion

ShibataShibata

良い記事ありがとうございます!

children を使う

は少し慣れが必要そうですね。。

├── app
│   ├── favicon.ico
│   ├── globals.css
│   ├── layout.tsx
│   ├── page.module.css
│   ├── page.tsx
│   └── samples
│       ├── ClientComponent.tsx
│       ├── ServerComponent.tsx
│       └── page.tsx

page.tsx

import ClientComponent from "./ClientComponent";
import ServerComponent from "./ServerComponent";

export default function Sample() {
  return (
    <>
      <ClientComponent>
        <ServerComponent />
      </ClientComponent>
    </>
  );
}

ServerComponent.tsx

export default function ServerComponent() {
  return <>Hello</>;
}

ClientComponent.tsx

"use client";

import { ReactNode } from "react";

type Props = { children: ReactNode };

export default function ClientComponent({ children }: Props) {
  return <>{children}</>;
}