🥦

ざっくりApp Router入門【Next.js】

2023/09/19に公開

App Routerについて色々調べたので、「Page Routerからどう変わったか?」を中心にいろいろ書きます。

※Next.jsのバージョンは13.4です

🤗Page Routerから変わった点

「これだけ抑えておけばOK」な内容についてザックリ書きます。

特別な意味を持つファイルができた

Page Routerではpages直下につくったファイルがすべてルーティングされました。

ですがApp Routerでは、app直下のpage.tsxと書かれたファイルだけがルーティングの対象になります。

ほかにも、layout.tsxと書かれたファイルを置いておくと「layout.tsxpage.tsxを自動的にラップする」ができたりします👇

📁app
 └📝layout.tsx
 └📝page.tsx

よく使うのはこの2つだと思いますが、他にも特別な意味を持つファイルがたくさん用意されています👇

https://nextjs.org/docs/getting-started/project-structure#app-routing-conventions

すべてサーバーコンポーネントになった

App Routerでは、すべてのコンポーネントがサーバーコンポーネントになりました。

ファイルの先頭に"use client"と書くことで、クライアントコンポーネントにすることもできます。

たとえば、以下のように書くとデフォルトでサーバーコンポーネントになりますが👇

page.tsx
export default function Home() {
  return <main>home</main>;
}

以下のようにファイルの先頭に"use client"と書くとクライアントコンポーネントになります👇

page.tsx
"use client"
export default function Home() {
  return <main>home</main>;
}
  • サーバーコンポーネント:
    「先にサーバー側でレンダリングしてからクライアントに送ろう!」なコンポーネントのこと
  • クライアントコンポーネント(従来のコンポーネント):
    「クライアント側でレンダリングしよう!」なコンポーネントのこと

この2つの違いが分からない人は、こちらの記事をオススメします👇
一言で理解するReact Server Components

この概念が分からないままApp Routerを使うと99%ハマると思うので、先に理解することをオススメします!

特別な意味を持つディレクトリが増えた

4つくらい増えました。

1. 論理グループ

ディレクトリ名を()で囲うと、ルーティングパスに影響されなくなる機能です。


引用:https://nextjs.org/docs/app/building-your-application/routing/route-groups

👆この場合、(marketing)(shop)はパスに影響されません。

2. プライベートフォルダ

ディレクトリ名の先頭に_をつけると、そのディレクトリすべてがルーティングから除外される機能です。

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

👆この場合、_lib配下はすべてルーティングから除外されます。

3. Parallel Routes

ディレクトリ名の先頭に@をつけると、1つのページの中に複数のページを表示できる機能です。


引用:https://nextjs.org/docs/app/building-your-application/routing/parallel-routes
ページが分かれているので、以下のようにerror.tsxなどを別々に指定できたりします👇

引用:https://nextjs.org/docs/app/building-your-application/routing/parallel-routes

4. Intercepting Routes

現在のレイアウトを保ったまま別ページを表示させることができる機能です。

たとえば、以下のように配置すると👇


引用:https://nextjs.org/docs/app/building-your-application/routing/intercepting-routes

以下のような動きになります👇

「/feedにいるときに/photo/123に移動すると、本来ならphoto/[id]/page.jsの内容が表示されるはずだけど、/feedのレイアウトを保ったまま(..)photo/[id]/pageの内容が表示される」

これを利用することで、以下のような動きを実現できます👇

  • /から/photos/2にアクセスした場合:
    /のレイアウトをそのままに写真を表示できる

  • /photos/2に直接アクセスした場合:
    →写真だけが表示される

UXにこだわりたい人向けの機能な気がします。

詳しく知りたい場合は、以下の記事が参考になります👇
Next.js の Interception Routes について

公式Exampleはこちら👇
https://nextgram.vercel.app/(GitHub)

getStaticPropsとgetServerSideProps がなくなった

以下のように変わりました👇

  1. getServerSideProps → 廃止
  2. getStaticProps → 廃止
  3. getStaticPathsgenerateStaticParams

サーバーコンポーネントという概念を理解できていれば「そりゃそうだよね」という話ですが、サーバーコンポーネント内で直接非同期処理を書けるので1.と2.は「もういらないよね」となりました。

SEOタグを簡単に設定できるようになった

page.tsxlayout.tsxで、metadataというオブジェクトをexportするだけで、SEOタグができるようになりました👇

import type { Metadata } from 'next'
 
export const metadata: Metadata = {
  title: '...',
  description: '...',
}

動的に設定したい場合は、generateMetadataという関数をexportします。

OGPが簡単に設定できるようになった

以下のファイル名の画像をpage.tsxlayout.tsxと同階層に置くだけで、OGPが設定できるようになりました👇

  • opengraph-image.(jpg|jpeg|png|gif)
  • twitter-image.(jpg|jpeg|png|gif)

拡張子を以下のようにすることで、動的にOGPを生成することもできます👇

  • opengraph-image.(js|ts|tsx)
  • twitter-image.(js|ts|tsx)

ルーティング系のフックがいろいろ変わった

以下のように変わりました👇

  • next/routerからではなく、next/navigationからuseRouterをimportするようになった
  • 以下のフックが追加された
    • useSearchParams
    • usePathname
    • useParams
  • これらすべてのフックはクライアントコンポーネントでしか使えなくなった
    • なので"use client"なファイルに切り出す必要がある

SSR / ISR の設定方法が変わった

Page Routerのときは、以下の2つでSSR / ISRの設定ができました👇

  • getServerSideProps
  • getStaticProps

ですが先述した通り、これらは廃止されました。

その代わりにfetchのオプションでSSR / ISR の設定ができるようになりました。

どういうことかというと、まずサーバーコンポーネント内でfetch関数を使った場合、基本的に結果がキャッシュされるようになりました。

たとえば、以下のように書くと、永続的にキャッシュされます。

fetch('https://...') //🟢

たとえば、以下のように書くと、1時間だけキャッシュされます。

fetch('https://...', { next: { revalidate: 3600 } }) //🟡

たとえば、以下のように書くと、キャッシュされずに毎回fetchされます。

fetch('https://...', { cache: 'no-store' }) //🔴

この場合、以下のようになります。

  • 🟢のようなfetchしか存在しないページ:SSG
  • 🟡のようなfetchが存在するページ:ISR
  • 🔴のようなfetchが存在するページ:SSR

また、fetch以外の処理(たとえばDBからのデータ取得)もcacheという関数でラップすることで、キャッシュできます👇

import { cache } from 'react'
 
export const revalidate = 3600 // revalidate the data at most every hour
 
export const getItem = cache(async (id: string) => {
  const item = await db.item.findUnique({ id })
  return item
})

具体的に

  • どういうときにキャッシュされるのか?
  • どういうときにキャッシュが破棄するのか?

については、書き方のパターンがいくつもあります。

たとえば以下のように書くことで、コンポーネント単位でfetchのオプションを設定できたりしますし(これは従来のgetStaticPropsrevalidateを設定するのと同等)、

page.tsx
...
export const revalidate = 3600
...

cookieを使うようなコードを書いた場合、「ユーザーによって表示させる内容は違うよね」と自動で判断されるので、キャッシュされなかったりします。

fetchしたデータ → コンポーネント間で受け渡す必要なし

親と子で同じ内容をそれぞれfetchした場合、「1回だけ実行すればいいよね」と自動的に判断されて、1回しかfetchされません。

この仕組みを「Request Memoization」と呼びます。

なのでわざわざ「二重fetch防止のために親でfetchしたデータを子に渡す」みたいなことをする必要はありません。

公式ドキュメントでは「好きな場所で好きなだけfetchしたらいいんだぜ」(意訳)と書かれています👇

deeple翻訳
ツリー内の複数のコンポーネントで同じデータ(現在のユーザーなど)を使用する必要がある場合、グローバルにデータをフェッチしたり、コンポーネント間でpropを転送したりする必要はありません。
代わりに、同じデータに対して複数のリクエストを行うことによるパフォーマンスへの影響を心配することなく、データを必要とするコンポーネントでフェッチまたはReactキャッシュを使用できます。

SSGの設定方法が変わった

以下のように変わりました。

  • 🚫Page Router
    next build & next exportを実行する。

  • ✅App Router
    next.config.jsoutput: "export"を追加して、next buildを実行する。
    next exportが不要になった)

🤔知っておいた方が良さげなこと

「これ知っておいたほうが良いな」と思ったことをいろいろ書きます。

従来のUIコンポーネントはサーバーコンポーネントに非対応

あくまで「現状は非対応」という話なのですが、たとえば以下などのUIコンポーネントはApp Routerに対応していません。

  • Chakra UI
  • MUI

対応していない理由は「これらのライブラリはemotionに依存していて、かつemotionはサーバーコンポーネントで動作しない。なのでApp Routerに対応していない。」になります。

なので逆に言えば、"use client"を付けたクライアントコンポーネントの中であれば、これらのUIコンポーネントは今まで通り使えます。

たとえば、Chakra UIのページでも「"use client"を付けてChakraProviderをラップしたらいいぜ」と案内されています👇

https://chakra-ui.com/getting-started/nextjs-guide

ただ、このやり方だとサーバーコンポーネントのメリットを殺してしまいます。

なのでサーバーコンポーネントのメリットを活かすなら、現状だと

  • 何らかのHeadlessコンポーネント(たとえばRadix UIなど)+何らかのzero-runtime系のCSSフレームワーク(たとえばTailwind CSSなど)

を組み合わせるのがベターなのかなと思います。

サーバーコンポーネントとクライアントコンポーネントの混合

基本的には以下のとおりです。

  • ✅クライアントコンポーネントからクライアントコンポーネントを呼び出す
  • ✅サーバーコンポーネント  からクライアントコンポーネントを呼び出す
  • ❌クライアントコンポーネントからサーバーコンポーネント  を呼び出す

ただし、最後のケースにおいても、クライアントコンポーネントのchildrenとしてサーバーコンポーネントを渡すことで✅にできます。

https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns#supported-pattern-passing-server-components-to-client-components-as-props

Instrumentation

「サーバー起動時に1回だけ実行したい!」な処理を書ける機能です。

以下のような感じでプロジェクトの直下にinstrumentation.tsというファイルを用意して、registerという関数をexportするだけです。

instrumentation.ts
export function register() {
 //サーバー起動時に1回だけ実行したい処理
}

たとえば「サーバーリッスンする前にDBに接続する」な用途に使えそうです。

  • 今の時点でまだexperimentalなので、next.config.jsをゴニョゴニョする必要あり。
  • プロジェクトの直下に置くので、Page Routerのプロジェクトでも使えます。

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

Server Actions

一言でいうと「クライアント側のコードにサーバー側のコードを直接書ける機能」です。

以下、公式のサンプルです。

import db from './db';
import { redirect } from 'next/navigation';
 
async function create(formData: FormData) {
  'use server';
  const post = await db.post.insert({
    title: formData.get('title'),
    content: formData.get('content'),
  });
  redirect(`/blog/${post.slug}`);
}
 
export default function Page() {
  return (
    <form action={create}>
      <input type="text" name="title" />
      <textarea name="content" />
      <button type="submit">Submit</button>
    </form>
  );
}

//https://nextjs.org/blog/next-13-4#server-actions-alpha

ヤバイですよねこれ。異次元の便利さです。

現時点でまだαなので本番で使うのは厳しいですが、stabledになったら小規模~中規模くらいのプロジェクトは「もう全部Next.jsでいいじゃん」となるかもなので、「こういうのがあるんだな」くらいには知っておいた方が良いと思います。

日本語で解説してくれている方もいます👇
https://www.youtube.com/watch?v=gircIiBIopA

https://nextjs.org/docs/app/api-reference/functions/server-actions

4種類のキャッシュ

4種類のキャッシュの仕組みが存在します👇

引用:https://nextjs.org/docs/app/building-your-application/caching

雑にまとめると、以下のような感じです👇

  • 🟥Router Cache
    • 「一度表示したページはクライアント側にキャッシュがあるんだから、それ使えばサーバーにリクエスト送らなくてよくなるじゃん」なやつ。
    • ただし、永続的にキャッシュが有効なわけではなくて以下のケースで無効化される。
      • 任意の時間が経ったとき(たとえばStatic Routeなページだと5分)。
      • ページをリロードしたとき。
  • 🟦Full Route Cache
    • 「一度生成したページはサーバー側にキャッシュがあるんだから、リクエストされたらそれ返せばいいじゃん!」なやつ。
    • 基本的には永続化されるけど、以下のケースだと永続化されない。
      • fetchでrevalidateなどを指定している場合 → 指定したタイミングでキャッシュが再生成されたり、そもそも生成されなかったりする
      • cookies関数のような「ユーザーによって返す値が違ってくるじゃん」な関数(Dynamic Functionsと呼ぶ)を使っている場合 → そもそもキャッシュが生成されない。
  • 🟨Request Memoization
    • 「複数のコンポーネントで同じ内容を何回もfetchしてるぞ~?よーし、1つにまとめちゃえ!」なやつ。
    • Next.jsの機能ではなく、Reactの機能。
    • GETメソッドにのみ適用される。
    • POSTメソッドやDBへの取得処理などもメモ化したい場合は、Reactのcacheを使う。
  • 🟪Data Cache
    • 「これさっきfetchしたやつと同じだから結果を使いまわしたろ!」なやつ。
    • 基本的には永続化されるけど、以下のケースだと永続化されない。
      • fetchでrevalidateなどを指定している場合 → 指定したタイミングでキャッシュが再生成されたり、そもそも生成されなかったりする
    • revalidatePathなどを使えば任意のタイミングでキャッシュを削除できる。

🟥と🟨は、すぐに消えるしユーザー単位なので、あまり意識しなくていいのかも。

🟦と🟪は、基本的にユーザー全体&デプロイメントに渡って永続化されるので、意識しないとバグになるかも。

ちなみに🟦は1つのデプロイメントでだけ有効ですが、🟪は複数のデプロイメント間に渡って有効らしいです。なので🟪は再デプロイしても残ります。

server only

server onlyというパッケージをnpm installして、たとえばA.tsxの先頭にimport 'server-only'と書きます。

そうすると、A.tsxをクライアントコンポーネントから呼び出している場合にビルドエラーが出るようにできます。

https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns#keeping-server-only-code-out-of-the-client-environment

Linkコンポーネントのhrefプロパティの型チェックを有効にできるやつです。

  • 今の時点でまだexperimentalなので、next.config.jsをゴニョゴニョする必要あり。

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

Page Router → App Routerへの移行

ガイドがあります👇
https://nextjs.org/docs/app/building-your-application/upgrading/app-router-migration

おわり

Discussion