Next.jsでのクライアント側ルーティング
公開: 2022-03-10ハイパーリンクは、Webの開始以来、宝石の1つです。 MDNによると、ハイパーリンクはWebをWebにするものです。 ドキュメント間のリンクなどの目的で使用されますが、その主な用途は、一意のWebアドレスまたはURLで識別可能なさまざまなWebページを参照することです。
ルーティングは、Webへのハイパーリンクと同様に、各Webアプリケーションの重要な側面です。 これは、リクエストを処理するコードにリクエストをルーティングするメカニズムです。 ルーティングに関連して、Next.jsページは、一意のURLパスによって参照および識別できます。 Webがハイパーリンクで相互接続されたナビゲーションWebページで構成されている場合、各Next.jsアプリは、ルーターで相互接続されたルーティング可能なページ(ルートハンドラーまたはルート)で構成されます。
Next.jsには、特にレンダリングとデータフェッチを検討する場合に、解凍するのが扱いにくいルーティングのサポートが組み込まれています。 Next.jsでのクライアント側ルーティングを理解するための前提条件として、Next.jsでのルーティング、レンダリング、データフェッチなどの概念の概要を理解する必要があります。
この記事は、Next.jsに精通していて、ルーティングの処理方法を学びたいReact開発者にとって有益です。 この記事を最大限に活用するには、ReactとNext.jsの実用的な知識が必要です。この記事は、Next.jsのクライアント側ルーティングと関連する概念についてのみ説明しています。
ルーティングとレンダリング
ルーティングとレンダリングは相互に補完的であり、この記事の過程で大きな役割を果たします。 Gauravがそれらを説明する方法が好きです:
ルーティングは、ユーザーがWebサイトのさまざまなページに移動するプロセスです。
レンダリングは、それらのページをUIに配置するプロセスです。 特定のページへのルートをリクエストするたびに、そのページもレンダリングされますが、すべてのレンダリングがルートの結果であるとは限りません。
それについて考えるのに5分かかります。
Next.jsでのレンダリングについて理解する必要があるのは、ハイドレーションと呼ばれるプロセスを通じて完全にインタラクティブになるために必要な最小限のJavaScriptコードとともに、各ページが事前にレンダリングされることです。 Next.jsがこれをどのように行うかは、事前レンダリングの形式に大きく依存します。静的生成またはサーバー側レンダリングは、使用されるデータフェッチ手法と高度に結合されており、ページのHTMLが生成されるときに分離されます。
データフェッチの要件によっては、 getStaticProps
、 getStaticPaths
、 getServerSideProps
などの組み込みのデータフェッチ関数、SWR、react-queryなどのクライアント側のデータフェッチツール、またはfetch-on-などの従来のデータフェッチアプローチを使用している場合があります。レンダリング、フェッチしてからレンダリング、フェッチとしてレンダリング(サスペンスを使用)。
事前レンダリング(レンダリング前— UIへ)はルーティングを補完し、データフェッチと高度に結合されています—Next.jsの独自のトピック全体です。 したがって、これらの概念は補完的または密接に関連していますが、この記事では、必要に応じて関連する概念を参照しながら、ページ間の単なるナビゲーション(ルーティング)にのみ焦点を当てます。
それが邪魔にならないように、基本的な要点から始めましょう。Next.jsには、ページの概念に基づいて構築されたファイルシステムベースのルーターがあります。
ページ
Next.jsのページは、ルートとして自動的に利用できるReactコンポーネントです。 これらは、 .js
、 .jsx
、 .ts
、 .tsx
などのサポートされているファイル拡張子を持つpagesディレクトリからデフォルトのエクスポートとしてエクスポートされます。
典型的なNext.jsアプリは、 pages 、 public 、 stylesなどのトップレベルのディレクトリを持つフォルダー構造になります。
next-app ├── node_modules ├── pages │ ├── index.js // path: base-url (/) │ ├── books.jsx // path: /books │ └── book.ts // path: /book ├── public ├── styles ├── .gitignore ├── package.json └── README.md
各ページはReactコンポーネントです。
// pages/books.js — `base-url/book` export default function Book() { return
本
}
注:ページは「ルートハンドラー」と呼ばれることもあることに注意してください。
カスタムページ
これらは、 pagesディレクトリにあるが、ルーティングには参加しない特別なページです。 _app.js
、 _document.js
のように、接頭辞としてアンダースコア記号が付きます。
-
_app.js
これは、pagesフォルダーにあるカスタムコンポーネントです。 Next.jsは、このコンポーネントを使用してページを初期化します。 -
_document.js
_app.js
と同様に、_document.js
は、Next.jsがアプリケーションの<html>
>タグと<body>
タグを拡張するために使用するカスタムコンポーネントです。 Next.jsページは周囲のドキュメントのマークアップの定義をスキップするため、これが必要です。
next-app ├── node_modules ├── pages │ ├── _app.js // ️ Custom page (unavailable as a route) │ ├── _document.jsx // ️ Custom page (unavailable as a route) │ └── index.ts // path: base-url (/) ├── public ├── styles ├── .gitignore ├── package.json └── README.md
ページ間のリンク
Next.jsは、ページ間のクライアント側のルート遷移を実行するために使用できるnext/link
linkAPIからのLink
コンポーネントを公開します。
// Import the <Link/> component import Link from "next/link"; // This could be a page component export default function TopNav() { return ( <nav> <Link href="/">Home</Link> <Link href="/">Publications</Link> <Link href="/">About</Link> </nav> ) } // This could be a non-page component export default function Publications() { return ( <section> <TopNav/> {/* ... */} </section> ) }
Link
コンポーネントは、ページ内かどうかに関係なく、任意のコンポーネント内で使用できます。 上記の例のように最も基本的な形式で使用すると、 Link
コンポーネントはhref
属性を持つハイパーリンクに変換されます。 ( Link
の詳細については、以下の次の/リンクセクションを参照してください。)
ルーティング
Next.jsファイルベースのルーティングシステムを使用して、最も一般的なルートパターンを定義できます。 これらのパターンに対応するために、各ルートはその定義に基づいて分離されています。
インデックスルート
デフォルトでは、Next.jsアプリでは、初期/デフォルトルートはpages/index.js
であり、これは自動的にアプリケーションの開始点として/
として機能します。 ベースURLがlocalhost:3000
の場合、このインデックスルートには、ブラウザのアプリケーションのベースURLレベルでアクセスできます。
インデックスルートは、各ディレクトリのデフォルトルートとして自動的に機能し、名前の冗長性を排除できます。 以下のディレクトリ構造は、 /
と/home
の2つのルートパスを公開しています。
next-app └── pages ├── index.js // path: base-url (/) └── home.js // path: /home
ネストされたルートを使用すると、削除がより明確になります。
ネストされたルート
pages/book
のようなルートは1レベルの深さです。 さらに深く進むには、ネストされたルートを作成する必要があります。これには、ネストされたフォルダー構造が必要です。 https://www.smashingmagazine.com
のベースURLを使用して、次のようなフォルダー構造を作成することにより、ルートhttps://www.smashingmagazine.com/printed-books/printed-books
にアクセスできます。
next-app └── pages ├── index.js // top index route └── printed-books // nested route └── printed-books.js // path: /printed-books/printed-books
または、インデックスルートを使用してパスの冗長性を排除し、 https://www.smashingmagazine.com/printed-books
で印刷された書籍のルートにアクセスします。
next-app └── pages ├── index.js // top index route └── printed-books // nested route └── index.js // path: /printed-books
動的ルートも、冗長性を排除する上で重要な役割を果たします。
動的ルート
前の例から、インデックスルートを使用してすべての印刷された本にアクセスします。 個々の本にアクセスするには、次のように本ごとに異なるルートを作成する必要があります。
// ️ Don't do this. next-app └── pages ├── index.js // top index route └── printed-books // nested route ├── index.js // path: /printed-books ├── typesript-in-50-lessons.js // path: /printed-books/typesript-in-50-lessons ├── checklist-cards.js // path: /printed-books/checklist-cards ├── ethical-design-handbook.js // path: /printed-books/ethical-design-handbook ├── inclusive-components.js // path: /printed-books/inclusive-components └── click.js // path: /printed-books/click
これは非常に冗長でスケーラブルではなく、次のような動的ルートで修正できます。
// Do this instead. next-app └── pages ├── index.js // top index route └── printed-books ├── index.js // path: /printed-books └── [book-id].js // path: /printed-books/:book-id
角かっこ構文— [book-id]
—は動的セグメントであり、ファイルだけに限定されません。 また、以下の例のようなフォルダーで使用して、作成者をルート/printed-books/:book-id/author
で利用できるようにすることもできます。
next-app └── pages ├── index.js // top index route └── printed-books ├── index.js // path: /printed-books └── [book-id] └── author.js // path: /printed-books/:book-id/author
ルートの動的セグメントは、 useRouter()
フックのquery
オブジェクトを使用してルートに含まれる接続コンポーネントのいずれかでアクセスできるクエリパラメータとして公開されます—(これについては次の/ルーターAPIセクションで詳しく説明します) )。
// printed-books/:book-id import { useRouter } from 'next/router'; export default function Book() { const { query } = useRouter(); return ( <div> <h1> book-id <em>{query['book-id']}</em> </h1> </div> ); }
// /printed-books/:book-id/author import { useRouter } from 'next/router'; export default function Author() { const { query } = useRouter(); return ( <div> <h1> Fetch author with book-id <em>{query['book-id']}</em> </h1> </div> ); }
すべてのルートをキャッチして動的ルートセグメントを拡張する
[book-id].js
を使用した前の例のように、動的ルートセグメントブラケットの構文を見てきました。 この構文の利点は、 Catch-AllRoutesを使用するとさらに処理が進むことです。 名前からこれが何をするかを推測することができます:それはすべてのルートをキャッチします。
動的な例を見ると、IDを使用して複数の本にアクセスする単一のルートでファイル作成の冗長性を排除するのにどのように役立つかがわかりました。 しかし、私たちにできることは他にもあります。
具体的には、パス/printed-books/:book-id
があり、ディレクトリ構造は次のとおりです。
next-app └── pages ├── index.js └── printed-books ├── index.js └── [book-id].js
パスを更新してカテゴリなどのセグメントを増やすと、次のようになる可能性があります/printed-books/design/:book-id
、さらには/printed-books/engineering/:book-id
/printed-books/:category/:book-id
。
リリース年を追加しましょう: /printed-books/:category/:release-year/:book-id
。 パターンが見えますか? ディレクトリ構造は次のようになります。
next-app └── pages ├── index.js └── printed-books └── [category] └── [release-year] └── [book-id].js
動的ルートの代わりに名前付きファイルを使用しましたが、どういうわけか、別の形式の冗長性が得られました。 まあ、修正があります:深くネストされたルートの必要性を排除するすべてのルートをキャッチします:
next-app └── pages ├── index.js └── printed-books └── [...slug].js
接頭辞が3つのドットである点を除いて、同じ角かっこ構文を使用します。 ドットはJavaScriptのスプレッド構文のように考えてください。 不思議に思うかもしれませんが、キャッチオールルートを使用する場合、カテゴリ( [category]
)とリリース年( [release-year]
)にアクセスするにはどうすればよいですか。 ふたつのやり方:
- 印刷された本の例の場合、最終目標は本であり、各本の情報にはメタデータが添付されます。
- 「スラッグ」セグメントは、クエリパラメータの配列として返されます。
import { useRouter } from 'next/router'; export default function Book() { const { query } = useRouter(); // There's a brief moment where `slug` is undefined // so we use the Optional Chaining (?.) and Nullish coalescing operator (??) // to check if slug is undefined, then fall back to an empty array const [category, releaseYear, bookId] = query?.slug ?? []; return ( <table> <tbody> <tr> <th>Book Id</th> <td>{bookId}</td> </tr> <tr> <th>Category</th> <td>{category}</td> </tr> <tr> <th>Release Year</th> <td>{releaseYear}</td> </tr> </tbody> </table> ); }
ルート/printed-books/[…slug]
のその他の例を次に示します。
道 | クエリパラメータ |
---|---|
/printed-books/click.js | {「ナメクジ」:[「クリック」]} |
/printed-books/2020/click.js | {「スラッグ」:[「2020」、「クリック」]} |
/printed-books/design/2020/click.js | {「スラッグ」:[「デザイン」、「2020」、「クリック」]} |
キャッチオールルートの場合と同様に、フォールバックインデックスルートを指定しない限り、ルート/printed-books
は404エラーをスローします。
next-app └── pages ├── index.js └── printed-books ├── index.js // path: /printed-books └── [...slug].js
これは、キャッチオールルートが「厳密」であるためです。 スラッグと一致するか、エラーをスローします。 キャッチオールルートと一緒にインデックスルートを作成したくない場合は、代わりにオプションのキャッチオールルートを使用できます。
オプションのキャッチオールルートを使用した動的ルートセグメントの拡張
構文はcatch-all-routesと同じですが、代わりに二重角かっこが付いています。
next-app └── pages ├── index.js └── printed-books └── [[...slug]].js
この場合、キャッチオールルート(スラッグ)はオプションであり、使用できない場合は、クエリパラメータなしで[[…slug]].js
ルートハンドラーでレンダリングされたパス/printed-books
へのフォールバックです。
インデックスルートと一緒にキャッチオールを使用するか、オプションのキャッチオールルートのみを使用します。 キャッチオールルートとオプションのキャッチオールルートを一緒に使用することは避けてください。
ルートの優先順位
最も一般的なルーティングパターンを定義できる機能は、「ブラックスワン」である可能性があります。 ルートが衝突する可能性は、特に動的ルートを処理し始めたときに迫り来る脅威です。
そうすることが理にかなっている場合、Next.jsはエラーの形でルートの衝突について知らせます。 そうでない場合は、その特異性に応じてルートに優先順位が適用されます。
たとえば、同じレベルに複数の動的ルートがあるとエラーになります。
// This is an error // Failed to reload dynamic routes: Error: You cannot use different slug names for the // same dynamic path ('book-id' !== 'id'). next-app └── pages ├── index.js └── printed-books ├── [book-id].js └── [id].js
以下に定義されているルートをよく見ると、衝突の可能性に気付くでしょう。
// Directory structure flattened for simplicity next-app └── pages ├── index.js // index route (also a predefined route) └── printed-books ├── index.js ├── tags.js // predefined route ├── [book-id].js // handles dynamic route └── [...slug].js // handles catch all route
たとえば、次のように答えてみてください。パス/printed-books/inclusive-components
を処理するルートはどれですか。
-
/printed-books/[book-id].js
、または /printed-books/[…slug].js
。
その答えは、ルートハンドラーの「特異性」にあります。 事前定義されたルートが最初に来て、次に動的ルート、次にキャッチオールルートが続きます。 ルートリクエスト/処理モデルは、次の手順で擬似コードと考えることができます。
- ルートを処理できる事前定義されたルートハンドラーはありますか?
-
true
—ルートリクエストを処理します。 -
false
—2に進みます。
-
- ルートを処理できる動的ルートハンドラーはありますか?
-
true
—ルートリクエストを処理します。 -
false
—3に進みます。
-
- ルートを処理できるキャッチオールルートハンドラーはありますか?
-
true
—ルートリクエストを処理します。 -
false
—404ページが見つかりませんをスローします。
-
したがって、 /printed-books/[book-id].js
が優先されます。
その他の例は次のとおりです。
ルート | ルートハンドラー | ルートの種類 |
---|---|---|
/printed-books | /printed-books | インデックスルート |
/printed-books/tags | /printed-books/tags.js | 事前定義されたルート |
/printed-books/inclusive-components | /printed-books/[book-id].js | 動的ルート |
/printed-books/design/inclusive-components | /printed-books/[...slug].js | キャッチオールルート |
next/link
API
next/link
APIは、クライアント側のルート遷移を実行するための宣言型の方法としてLink
コンポーネントを公開します。
import Link from 'next/link' function TopNav() { return ( <nav> <Link href="/">Smashing Magazine</Link> <Link href="/articles">Articles</Link> <Link href="/guides">Guides</Link> <Link href="/printed-books">Books</Link> </nav> ) }
Link
コンポーネントは、通常のHTMLハイパーリンクに解決されます。 つまり、 <Link href="/">Smashing Magazine</Link>
は<a href="/">Smashing Magazine</a>
に解決されます。
href
小道具は、 Link
コンポーネントに必要な唯一の小道具です。 Link
コンポーネントで利用可能な小道具の完全なリストについては、ドキュメントを参照してください。
Link
コンポーネントには、他にも注意すべきメカニズムがあります。
動的セグメントのあるルート
Link
9.5.3より前では、動的ルートへのリンクは、次のようにhref
とLink
へas
小道具の両方を提供する必要があったことを意味していました。
import Link from 'next/link'; const printedBooks = [ { name: 'Ethical Design', id: 'ethical-design' }, { name: 'Design Systems', id: 'design-systems' }, ]; export default function PrintedBooks() { return printedBooks.map((printedBook) => ( <Link href="/printed-books/[printed-book-id]" as={`/printed-books/${printedBook.id}`} > {printedBook.name} </Link> )); }
これにより、Next.jsは動的パラメーターのhrefを補間できましたが、面倒でエラーが発生しやすく、やや必須であり、Next.js10のリリースでほとんどのユースケースで修正されました。
この修正には下位互換性もあります。 as
とhref
の両方を使用している場合、何も壊れません。 新しい構文を採用するには、次の例のように、 href
propとその値as
破棄し、aspropの名前をhref
に変更します。
import Link from 'next/link'; const printedBooks = [ { name: 'Ethical Design', id: 'ethical-design' }, { name: 'Design Systems', id: 'design-systems' }, ]; export default function PrintedBooks() { return printedBooks.map((printedBook) => ( <Link href={`/printed-books/${printedBook.id}`}>{printedBook.name}</Link> )); }
hrefの自動解決を参照してください。
passHref
プロップのユースケース
以下のスニペットをよく見てください。
import Link from 'next/link'; const printedBooks = [ { name: 'Ethical Design', id: 'ethical-design' }, { name: 'Design Systems', id: 'design-systems' }, ]; // Say this has some sort of base styling attached function CustomLink({ href, name }) { return <a href={href}>{name}</a>; } export default function PrintedBooks() { return printedBooks.map((printedBook) => ( <Link href={`/printed-books/${printedBook.id}`} passHref> <CustomLink name={printedBook.name} /> </Link> )); }
passHref
プロップは、 Link
コンポーネントにhref
プロップをCustomLink
子コンポーネントに渡すように強制します。 Link
コンポーネントが、ハイパーリンク<a>
タグを返すコンポーネントをラップする場合、これは必須です。 ユースケースは、styled-componentsのようなライブラリを使用している場合、または1つの子のみを想定しているため、複数の子をLink
コンポーネントに渡す必要がある場合に発生する可能性があります。
詳細については、ドキュメントを参照してください。
URLオブジェクト
Link
コンポーネントのhref
は、URL文字列に自動的にフォーマットされるquery
などのプロパティを持つURLオブジェクトにすることもできます。
printedBooks
オブジェクトを使用すると、以下の例は次のようにリンクされます。
-
/printed-books/ethical-design?name=Ethical+Design
and -
/printed-books/design-systems?name=Design+Systems
。
import Link from 'next/link'; const printedBooks = [ { name: 'Ethical Design', id: 'ethical-design' }, { name: 'Design Systems', id: 'design-systems' }, ]; export default function PrintedBooks() { return printedBooks.map((printedBook) => ( <Link href={{ pathname: `/printed-books/${printedBook.id}`, query: { name: `${printedBook.name}` }, }} > {printedBook.name} </Link> )); }
pathname
に動的セグメントを含める場合は、クエリがpathname
に補間されるように、クエリオブジェクトのプロパティとしても含める必要があります。
import Link from 'next/link'; const printedBooks = [ { name: 'Ethical Design', id: 'ethical-design' }, { name: 'Design Systems', id: 'design-systems' }, ]; // In this case the dynamic segment `[book-id]` in pathname // maps directly to the query param `book-id` export default function PrintedBooks() { return printedBooks.map((printedBook) => ( <Link href={{ pathname: `/printed-books/[book-id]`, query: { 'book-id': `${printedBook.id}` }, }} > {printedBook.name} </Link> )); }
上記の例にはパスがあります。
-
/printed-books/ethical-design
、および /printed-books/design-systems
。
VSCodeでhref
属性を調べると、タイプLinkProps
が見つかり、 href
プロパティはUrl
タイプであり、前述のようにstring
またはUrlObject
のいずれかです。
UrlObject
を検査すると、さらに次のプロパティを持つインターフェイスが表示されます。
これらのプロパティの詳細については、Node.jsURLモジュールのドキュメントをご覧ください。
ハッシュのユースケースの1つは、ページ内の特定のセクションにリンクすることです。
import Link from 'next/link'; const printedBooks = [{ name: 'Ethical Design', id: 'ethical-design' }]; export default function PrintedBooks() { return printedBooks.map((printedBook) => ( <Link href={{ pathname: `/printed-books/${printedBook.id}`, hash: 'faq', }} > {printedBook.name} </Link> )); }
ハイパーリンクは/printed-books/ethical-design#faq
に解決されます。
詳細については、ドキュメントをご覧ください。
next/router
API
next/link
が宣言型の場合、 next/router
は必須です。 関数コンポーネント内のrouter
オブジェクトへのアクセスを可能にするuseRouter
フックを公開します。 このフックを使用して手動でルーティングを実行できます。特に、 next/link
が十分でない場合や、ルーティングに「フック」する必要がある場合に使用できます。
import { useRouter } from 'next/router'; export default function Home() { const router = useRouter(); function handleClick(e) { e.preventDefault(); router.push(href); } return ( <button type="button" onClick={handleClick}>Click me</button> ) }
useRouter
はReactフックであり、クラスでは使用できません。 クラスコンポーネントにrouter
オブジェクトが必要ですか? withRouter
を使用します。
import { withRouter } from 'next/router'; function Home({router}) { function handleClick(e) { e.preventDefault(); router.push(href); } return ( <button type="button" onClick={handleClick}>Click me</button> ) } export default withRouter(Home);
router
オブジェクト
useRouter
フックとwithRouter
上位コンポーネントの両方が、現在のページのURL状態、 locale
、 locales
、およびdefaultLocale
に関する情報を提供するpathname
、 query
、 asPath
、 basePath
などのプロパティを持つルーターオブジェクトを返します。アクティブ、サポートされている、または現在のデフォルトロケール。
ルーターオブジェクトには、プッシュと同様に、履歴スタックに新しいURLエントリを追加して新しいURLに移動するためのpush
、 replace
などのメソッドもありますが、履歴スタックに新しいURLエントリを追加する代わりに現在のURLを置き換えます。
ルーターオブジェクトの詳細をご覧ください。
next.config.js
を使用したカスタムルート構成
これは、特定のNext.jsの動作を構成するために使用できる通常のNode.jsモジュールです。
module.exports = { // configuration options }
next.config.js
を更新するときはいつでも、サーバーを再起動することを忘れないでください。 もっと詳しく知る。
ベースパス
Next.jsの初期/デフォルトルートはpaths /
のpages/index.js
であると述べられました。 これは構成可能であり、デフォルトルートをドメインのサブパスにすることができます。
module.exports = { // old default path: / // new default path: /dashboard basePath: '/dashboard', };
これらの変更は、すべての/
パスが/dashboard
にルーティングされたアプリケーションで自動的に有効になります。
この機能は、Next.js9.5以降でのみ使用できます。 もっと詳しく知る。
末尾のスラッシュ
デフォルトでは、末尾のスラッシュは各URLの末尾で使用できません。 ただし、次の方法で切り替えることができます。
module.exports = { trailingSlash: true };
# trailingSlash: false /printed-books/ethical-design#faq # trailingSlash: true /printed-books/ethical-design/#faq
ベースパスと末尾のスラッシュ機能はどちらも、Next.js9.5以降でのみ使用できます。
結論
ルーティングはNext.jsアプリケーションの最も重要な部分の1つであり、ページの概念に基づいて構築されたファイルシステムベースのルーターに反映されます。 ページを使用して、最も一般的なルートパターンを定義できます。 ルーティングとレンダリングの概念は密接に関連しています。 独自のNext.jsアプリを作成したり、Next.jsコードベースで作業したりするときに、この記事のレッスンを受講してください。 詳細については、以下のリソースを確認してください。
関連リソース
- PagesのNext.js公式ドキュメント
- データフェッチに関するNext.jsの公式ドキュメント
- next.config.jsのNext.js公式ドキュメント
- Next.js 10:
href
の自動解決 - next/linkのNext.js公式ドキュメント
- next/routerのNext.js公式ドキュメント