AppRouterに触れてみよう!
初めに
今回は、Next.js の v13.4 からstable
となったAppRouter
を紹介します。
たくさんの新機能がありますが、本記事では TypeScript を使用して以下の 3 つの機能について紹介します。
- ルーティング
- データフェッチ
- サーバーコンポーネントとクライアントコンポーネント
それでは、順番に説明をしていきます。
ルーティング
これまでのNext.js
はpages
ディレクトリに作成したファイル名やディレクトリ名がそのまま URL のパスになっていましたが、これからはapp
ディレクトリ配下にディレクトリを作りファイル名はpage.ts
で書く必要があります。今までindex.ts
としていましたが、index という名前は意味をなさなくなります。
例:app/dashboard/page.ts
とした場合、/dashboard
の URL パスとなります。
ダイナミックルーティング(user/[id]/pages.ts
)は今まで通り使えます。
page.ts
以外にも重要な意味を持つファイル名が存在するのでいくつか紹介します。
- layout.ts
画面のレイアウトを作っていて複数の画面で同じ UI を使いたいということがあります。例えば、複数の画面で、ヘッダーとフッターを入れたい。などです。このようなときに各ページでヘッダーとフッターをインポートして使っていては少し面倒です。
export default function RootLayout({children}:{children:React.RectNode}) {
return (
<html lang="en">
<body>
{children}
</body>
</html>
);
}
app ディレクトリ直下に置かれたlayout.ts
ルートレイアウトと呼ばれ、必ず置いておく必要があり、ルートレイアウトは全てのページで共有されます。
今までの_app
と_document
がひとつになった感じです。
また、ルートレイアウトでは html タグと body タグが必須です。
さらに、ディレクトリごとにもlayout.ts
を作ることができます。
export default function DashboardLayout({children}: {children: React.ReactNode}) {
return{
<Header />
{children}
<Footer />
};
}
こうすることで dashboard ディレクトリ配下のすべてのpage.ts
でヘッダーとフッターが表示されます。
- loading.ts
名前を見てわかる通りローディング中の UI を共有できるものです。
export default function Loading() {
return <LoadingSkeleton />;
}
一番近い場所(同じ階層になければ、親のディレクトリをたどっていく)にあるloading.ts
が適用されます。
このほかにもローディング時に使えるSuspense
というものも紹介します。
そもそも、現状の問題点として、ページにアクセスする際、ページの全データを取得し、それを表示していますが、そのデータの中に取得に時間のかかるものがあるとそれを他が待ってしまい、表示に時間がかかってしまいます。
これを解決するために、Suspense
というものを使います。
import { Suspense } from 'react';
import { PostFeed, Weather } from './Components';
export default function Posts() {
return
<section>
<Suspense fallback={<p>Loading feed...</p>}>。
<PostFeed />
</Suspense>
<Suspense fallback={<p>Loading weather...</p>}>。
<Weather>
</Suspense>
</section>
);
}
Suspense
で非同期処理を行うコンポーネントをラップします。ラップされたコンポーネントを読み込んでいる間は、fallback
に渡した UI を表示します。そして、読み込みが完了したコンポーネントは順次画面に表示されます。
使い道として、ブログの本分とそれについたコメントをそれぞれ、Suspense
でラップし、それぞれが、それぞれを待つことなく読み込めた方から表示することができます。
他の使い方は、React のドキュメントで紹介されています。
- error.ts
これも、名前でわかります。
'use client';
import { useEffect } from 'react';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
useEffect(() => {
console.error(error);
}, [error]);
return (
<div>
<h2>Something went wrong!</h2>
<button
onClick={
() => reset()
}
>
Try again
</button>
</div>
);
}
dashboard
配下でエラーが発生した場合、このコンポーネントがlayout.ts
でラップされて表示されます
reset 関数を実行することで、このページを再レンダリングすることができます。
1行目の'use client'
は必ず書く必要があります。'use client'
の意味については後程紹介します。
レイアウトのエラー処理
error.ts
では、同階層にある、layout.ts
のエラーを補足することができません。
これを解決するためには、そのlayout.ts
の親のディレクトリにerror.ts
を置いておく必要があります。
ルートレイアウトのエラー処理
先ほど、layout.ts
のエラーを補足するには親のディレクトリにerror.ts
を置いておく必要があると紹介しましたが、一番上の階層である、ルートレイアウトはどのようにエラーを補足すればよいのでしょうか。
global-error.ts
を使用することで解決できます。
'use client';
export default function GlobalError({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</body>
</html>
);
}
- not-found.ts
not-found.ts
は存在しないページにアクセスした際のページの UI を定義できます。
export default function NotFound() {
return (
<>
<h2>Not Found</h2>
<p>Could not find requested resource</p>
</>
);
}
これも、各ディレクトリに置いておくことができます。
データフェッチ
App Router
では、fetch
API が拡張されており、キャッシュに関する設定をすることができます。
静的なデータの取得
静的なデータとは、どのユーザーにも同じ値を返すデータのことです。例えば、ブログの記事などがそれに該当します。
fetch("https://...", { cache: "force-cache" });
force-cache
を設定することで、まずキャッシュをみて一致するものがあれば、それを返します。
ちなみに、cache オプションのデフォルトがforce-cache
なので、書かなくてもよいです。
データの定期的な更新
cache: "force-cache"
ではキャッシュをみてあれば、それを返してしまうため、新しいデータがあっても、データが更新されることはありません。
キャッシュされたデータを一定時間ごとに更新するにはrevalidate
オプションを使用します。
fetch("https://...", { next: { revalidate: 10 } });
revalidate
に設定された、秒数が立つまでは何度リクエストがあっても、キャッシュの更新がされず、revalidate
に設定された秒数が立ってから次のリクエストのタイミングでキャッシュが更新されます。Next.js のドキュメントで
Note: This is equivalent to Static Site Generation (SSG) and Incremental Static Regeneration (ISR) in the Pages Router.
と書かれています。
今までは SSG,ISR で取得してきたデータは pages ディレクトリからバケツリレーでデータを渡していましたが、これからは、コンポーネントごとに SSG,ISR を使い分けることができるようになりました。
キャッシュを使用しない方法
最後にキャッシュを使用しない方法を紹介します。
fetch("https://...", { cache: "no-store" });
リクエストのたびに、キャッシュを見ずにサーバーからデータの取得をします。
サーバーコンポーネントとクライアントコンポーネント
まずは、サーバーコンポーネントとクライアントコンポーネントとは何かについて説明します。
この二つの違いは、コードが実行される場所です。それぞれ、サーバーとクライアントでコードが実行されます。
AppRouter のコンポーネントはデフォルトで サーバーコンポーネント になっています。
クライアントコンポーネント として使用するには、ファイルの一番上に'use client'
と記載する必要があります。
では、どのように使い分けるべきでしょうか。
Next.js のドキュメントの表をもとに紹介します。
サーバーコンポーネントの使いどころ
- データの取得
- バックエンドのリソースへのアクセス
- 機密情報へのアクセス(API キーなど)
- クライアントサイドの JavaScript を削減する
クライアントコンポーネントの使いどころ
- イベントリスナー(onClick など)
- ステートとライフサイクル(useState など)
- ブラウザ専用の API
- 上二つに依存するカスタムフック
主にユーザーのアクション(ボタンをクリックする、フォームに入力をする等)に対して動作するものはクライアントコンポーネントにする必要があります。
レンダリングの仕組み
サーバーコンポーネントとクライアントコンポーネントの入れ子にして使う場合の注意点を理解するには、どのようにレンダリングがされているのか知る必要があります。
- クライアントにコードを送信する前に、すべてのサーバーコンポーネントをレンダリングします。
- クライアントでは、クライアントコンポーネントをレンダリングし、サーバーとクライアントの情報を統合します。
ここで大事なのが、サーバーで処理をした後にクライアントで処理をするということです。
サーバーコンポーネントとクライアントコンポーネントをネストして使うときの制約
クライアントコンポーネントの中にサーバーコンポーネントをインポートしてはいけません。これをしてしまうと、サーバーコンポーネントをレンダリングした後、クライアントコンポーネントをレンダリングしようとしたときに、再度サーバーコンポーネントをレンダリングする必要が生じます。
'use client';
import ExampleServerComponent from './example-server-component';
export default function ExampleClientComponent({
children,
}: {
children: React.ReactNode;
}) {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
<ExampleServerComponent />
</>
);
}
これは非対応パターンとして Next.js のドキュメント で紹介されています。推奨パターンとしては
'use client';
import { useState } from 'react';
export default function ExampleClientComponent({
children,
}: {
children: React.ReactNode;
}) {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
{children}
</>
);
}
import ExampleClientComponent from './example-client-component';
import ExampleServerComponent from './example-server-component';
export default function Page() {
return (
<ExampleClientComponent>
<ExampleServerComponent />
</ExampleClientComponent>
);
}
このようにchildren
として渡すパターンです。こうすれば、<ExampleServerComponent>
を先にレンダリングさせることができます。
まとめ
Next.js v13.4 の AppRouter
について紹介しました。
新しいルーティングシステムでは、より直感的なファイルベースのルーティングを実現し、特別なファイル名を用いることでレイアウトやエラーハンドリングなどの制御を行うことができます。また、Suspense を使用することで、非同期のデータフェッチをスムーズに行うことが可能となりました。
データフェッチにおいては、fetchAPI の拡張により、キャッシュの利用や定期的な更新を簡単に設定できるようになりました。これにより、データの取得と表示をより効率的に行うことができます。
サーバーコンポーネントとクライアントコンポーネントの使い分けにより、パフォーマンスの最適化とユーザー体験の向上を図ることができます。
参考にしたサイト
Discussion