Next.jsのドキュメントを読む
よみます
- appディレクトリとpagesディレクトリは共存できる
- Imageコンポーネントが新しくなった
- Linkコンポーネントでaタグが不要に
- Scriptコンポーネントも何か変わったみたい
- next/fontでフォントの最適化ができるらしい。CSSでの@importと何が違うのかな?
PagesからAppへ
- getServerSideProps、getStaticProps、getStaticPathsはなくなった
- pages/_app.jsとpages/_document.jsはapp/layout.jsになった
- /pages/api/*はそのまま。
app/を作る
src/にapp/を作成。
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
appDir: true,
},
};
module.exports = nextConfig;
Root Layoutを作る
app/layout.tsxを作る。すべてのページに適用される。
ここに。<html>と<body>を定義する。
export default function RootLayout({
// Layouts must accept a children prop.
// This will be populated with nested layouts or pages
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
next/headはいらない
next/headは使わず以下のようにする
export const metadata = {
title: 'Home',
description: 'Welcome to Next.js',
};
metadataではogpやfaviconなど設定できるimport { Metadata } from 'next';
。
ページ
- appディレクトリのpageはデフォルトでServer Components
- ページはpage.tsxとして作る。
- ドキュメントではpage.tsxと同じ階層にComponentのtsxファイルを別に作り、page.tsxはimportするだけにしている。なぜ?page.tsxに直接書いちゃだめなのかな
- 多分、page.tsxではデータフェッチを行ってComponentにpropsとしてデータを渡す責務だけにするということなのかな?
Router
- ルーターは従来のnext/routerから、next/navigationからインポートする3つのフックに置き換わる。
- ルーターはclient componentのみで使う。
- dynamic routesのparamsはPageのparams propsで取得できる。
???
-
基本的にはServer Componentとしてサーバー側でデータフェッチをする?
-
クライアント側からSWRとかを使ってデータフェッチするときはどんなケースがある?
- SWRのキャッシュ機構を使いたいシーン
- 同じデータを再取得する際、いちいちローディング時間を発生させずにUXを向上できる
- ページレベルではないcomponent内で個別にデータ取得する時?
- データの変更が頻繁に起こるリアルタイム性のあるデータを表示するとき?
- キャッシュはNext.jsでもサポートされているらしい
- SWRのキャッシュ機構を使いたいシーン
-
ひょっとしてデータフェッチのほとんどのケースではSSRで事足りるのでは?
-
Server Componentでデータ取得してるローディング中にloading.tsxを表示できるってことなのかな??まだそこまで行ってないから分からないけど。
-
どこをSSRしてどこをCSRすればいいのかな?ややこしい。。
-
基本的にはSSRでデータ取得しちゃえばいいのじゃろうか??
-
POST, UPDATE, DELETEについては、従来通りクライアント側からAPIリクエストすればいいのかな?
わからん。。
つぎはここからよむ
データフェッチ
SSR
従来のgetServerSideProps
fetchのオプションで{ cache: 'no-store' }
を指定することで実現する
async function getProjects() {
const res = await fetch(`https://...`, { cache: 'no-store' });
const projects = await res.json();
return projects;
}
export default async function Dashboard() {
const projects = await getProjects();
header, cookie
新しい関数cookies()
とheaders()
を使って取得する。
SSG
従来のgetStaticProps
。
fetch関数はデフォルトでcache: 'force-cache'
になっているので、fetch関数をそのまま使用すれば良い。
const res = await fetch(`https://...`);
const projects = await res.json();
Dynamic paths (getStaticPaths)
getStaticPaths
がgenerateStaticParams
になりました。
ISR
{ next: { revalidate: 60 } }
オプションをfetch関数に入れる。
API Routes
app/api/ではRoute Handlersというのを使えるらしい。
なんと!
export async function GET(request: Request) {}
みたいに書けるらしい。🤯🤯🤯
多分またあとで詳しく出てくるので期待
Routing
✨新しいApp Routerの登場です✨
ルーティングはFile Based Routingと同じ感じ(ファイルは常に/page.tsxだけど)
ファイル規約
-
layout.js 同じ階層とその子階層の共通のUIを書ける。
- template.js 同じような動作をするが、ナビゲーション時に新しいコンポーネントインスタンスがマウントされる←ドユコト?
-
error.js エラーが発生したときに表示するUI
- global-error.js ルートのエラーで用いる?
- loading.js ページのローディング中のUIを書ける。
- not-found.js 404ページを置ける
- page.js UIを置く。ディレクトリ構造がそのままパスとなる。
- route.js APIのエンドポイントを書ける。
これらのファイルを置くと、実際にはコンポーネントの階層となってレンダリングされる。(route.jsはレンダリングに関係ないけど)
ここの図がわかりやすい:
なるほど〜
良い感じですね。
ディレクトリ内には普通にコンポーネントやCSS、storybookなど他のファイルも自由に置ける。
ページレベルじゃない小さなコンポーネントでもSuspenseを使いたいときは、普通にSuspenseを直書きする
- 共通のレイアウトがあるページ間の移動では、変更がある部分だけいい感じに部分レンダリングが行われるらしい。
Defining Routes
page.jsを置かなければパスは通らないので、page.jsのないディレクトリを作ってコンポーネントとか置ける!。
Route Groups
通常はディレクトリ構造がそのままパスにマッピングされるが、Route Groupsを使えばパスに影響しない形でルートを整理できる。
ルートレイアウトを複数作りたいときとかにも使える。(複数のルートレイアウト間を移動すると部分レンダリングではなくフルページロードされる)
Route Groupの作り方は、フォルダ名を括弧で囲む。(shop)/
みたいなかんじ
Dynamic Segments
フォルダ名を角括弧で囲む。 [slug]/
みたいなかんじ
その中身はparams
propsに渡されます。
pagesのDynamic Routesと同じ
-
[...folderName]/
とすると、それ以降のsegmentを配列で受け取れる。 -
shop/[[...folderName]]/
とすると、それ以降のsegmentを配列で受け取れる上に、そのsegmentが指定されていなくてもshop/でもアクセスできるようになる
Dynamic Segmentsに型を付ける場合はこう
export default function Page({
params,
}: {
params: { slug: string };
}) {
return <h1>My Page</h1>;
}
Pages
page.js(page.tsx)からcomponentをdefault exportすることで、ページを定義できます♪
page.jsでデータフェッチができるよ。これはあとで
Layouts
Layout:複数のページで共有されるUI
layout.js(layout.tsx)からcomponentをdefault exportすることで、レイアウトを定義できます♪
このcomponentはchildrenをpropsとして受け取って表示する必要がある
- 一番上のレイアウト、ルートレイアウトは、htmlタグやbodyタグを含める必要がある
- ルートレイアウトはclient componentにはできないよ
- レイアウトでもデータフェッチができるよ
- 親レイアウトから子レイアウトにはデータを受け渡せない。が、同じデータを複数回フェッチしても自動でキャッシュしてまとめてくれるのでパフォーマンスには影響を与えない。
- レイアウトはセグメント内で共有されるので、その下のセグメントにレイアウトがある場合、レイアウトはネストされる。
Templates
- Layoutと同じようなものだが、ページ間を移動したときに再レンダリングされる
- パフォーマンスの観点からなるべくLayoutを使うべき。
- CSSでアニメーションを入れたかったり、useEffectで何かしたかったりする時に使う。
ページの移動方法には、<Link>
とuseRouter()
のふたつある
<Link>
Component
- プリフェッチまでしてくれる
useRouter()
Hook
-
router.push('path/')
で移動できる - できるだけ
<Link>
を使いましょう
どっちも従来のものとほぼ変わらない。(Linkはaタグいらなくなったけど)
-
Server Componentは場合によってキャッシュしてくれる。
-
router.refresh()
で新しいリクエストをサーバーに送れる -
Hard Navigation 毎回サーバーから取得する
-
Soft Navigation キャッシュから取得してレンダリングする
-
Soft Navigationになる条件
- 遷移先がプリフェッチされている
- Dynamic Segmentsを含まない、または、Dynamic Segmentsでも現在のページと同じパラメータを持つ
-
戻る・進むを押したときもSoft Navigationになる。
リアルタイム性の高い情報を表示するときはキャッシュが邪魔になるときもありそうだけど、そういうときはrouter.refresh()
をすればいいのじゃろうか???
エラーハンドリング
- error.tsxはclient componentにする。
error.tsxの例
'use client'; // Error components must be Client components
import { useEffect } from 'react';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
useEffect(() => {
// Log the error to an error reporting service
console.error(error);
}, [error]);
return (
<div>
<h2>Something went wrong!</h2>
<button
onClick={
// Attempt to recover by trying to re-render the segment
() => reset()
}
>
Try again
</button>
</div>
);
}
- layout.jsやtemplate.jsでエラーが発生してもその階層のerror.jsは表示されない。layoutのエラーをキャッチしたければ、その上の階層にerror.jsを置く。
- ルートレイアウトのエラーをキャッチするには、app/global-error.jsを置く。
- global-error.jsでは独自のhtmlタグとbodyタグを定義する必要がある。
- Server Componentでエラーが発生した場合でも、直近のerror.jsが呼び出される
Route Handlers
待ってました🎉
import { NextResponse } from 'next/server';
export async function GET() {
const res = await fetch('https://data.mongodb-api.com/...', {
headers: {
'Content-Type': 'application/json',
'API-Key': process.env.DATA_API_KEY,
},
});
const data = await res.json();
return NextResponse.json({ data })
}
- ネストできるけど、page.jsと同じ階層には置けない
- GET以外にもPOST, PUT, PATCH, DELETE, HEAD, OPTIONSが使える。
- これでREST APIが簡単にできるよね?!
- と思ったけど、フォーム送信とかにはRoute Handlersは使わないほうがいいとある。なんで??
Parallel Routes
複数のページを同時に描画できるらしい
@folder/
の形式のフォルダ名にすることで使える
layout.jsでpropsとしてページが受け取って描画する。
Conditional Routes(条件付きルーティング)の実装に使える。
使うときになったら読もう
Intercepting Routes
サイドバーでカートを開いたり、モーダルでページを開いたりして、リロードしたりすると単一のページとして表示されるようなルーティングができる。
これも実装するときになったら読もう
Rountingを読み終わったので今日はここまで。
- App Routerにおいてのベストプラクティスとかなにもわからない。手探り状態
- 結局、pageレベルじゃない小さなコンポーネント内でData fetchするときはSuspense直書きでCSRするしかなくない?pageじゃないServer ComponentでのSSR/SSGができるのかいまいちよくわかってない。。
- Data Fetchingの章読めばそのあたりも多分分かるよね。
- 明日はRendering、Data Fetchingを読む。Data Fetchingが一番気になる。
- 読んだらとりあえず実装をしながらいろいろ試してみましょう
- おやすみ
レンダリング
- コンポーネントがレンダリングされる2つの環境:サーバー、クライアント
- コンポーネントレベルでレンダリング環境を選択できるようになった
- デフォルトはServer Components。クライアントに送信されるJavaScriptの量を減らせる。
- Server Componentの中にClient Componentを入れたり、その逆だったりができる。Reactがうまいこといい感じに処理してくれる
Static RenderingとDynamic Rendering
Static Rendering
ビルド時にClientとServer両方のComponentをプリレンダリングしてくれる。revalidateも可能。
SSG、ISRに相当する。
Dynamic Rendering
リクエスト毎にサーバー上でClient / Server Componentをレンダリングする。結果はキャッシュされない。
SSRに相当する。
???
Client Componentをサーバー上でレンダリングってどういうこと??
Server and Client Components
Server Components
- app routerではデフォルトでこれ
- JSのバンドルサイズを削減できる
Client Components
- クライアントサイドのインタラクティブ性を追加
- サーバー上でプリレンダリングされ、クライアント上でハイドレーションされる
- pages/で機能しているコンポーネントと同じ。
-
'use client';
で機能する -
use client
が定義されると、子コンポーネントを含め、そのファイルにインポートされたすべてのモジュールは、クライアントバンドルに含まれる
- Server Component:サーバー上でのみレンダリングされる
- Client Component:主にクライアントでレンダリングされるが、Next.jsではサーバーでプリレンダリングしてクライアントでハイドレーションすることもできる
Server ComponentとClient Componentの使い分け
- 基本的にはServer Componentを使う
- onClickやonChangeなどのイベントリスナー、useStateやuseEffectなどのフック、ブラウザのAPIなどを使うときにClient Componentを使う
- データフェッチはServer Componentが適している(Client Componentは△)
- Client Componentsでデータを取得することも可能だが、クライアントでデータを取得する特別な理由がない限り、Server Componentsでデータ取得したほうが良い
- その方がパフォーマンスとUXの向上につながるらしい。
- 共通のLayoutの中でインタラクティブなコンポーネントを使いたいときは、LayoutはServer Componentにして、インタラクティブな部分だけClient Componentとして切り離す
- Client Componentの中でServer Componentをインポートすることはできない。が、Client ComponentのchildrenとしてServer Componentを渡す形でwrappingすれば機能する。
- Server ComponentからClient Componentに渡すpropsには、関数やDate型の値は使えない。Serializationできる値のみを渡せる。
間違えてクライアントサイドにサーバー専用のコードが漏洩しないために
- たとえば環境変数でAPIキーなどを扱う際には、
API_KEY
のように、NEXT_PUBLIC_
をつけないことで、万が一環境変数を使ったコードがClient Componentにインポートされてしまっても、正しく実行されないで済む。 - また、サーバーコードを間違えてクライアントサイドにインポートしてしまったときにエラーを出すようにできるパッケージ
server-only
がある。- 該当の関数などのファイルの先頭に
import "server-only";
を入れると有効になる。
- 該当の関数などのファイルの先頭に
サードパーティ製パッケージの利用
-
use client
は新しい機能なので、サードパーティ製パッケージでは対応していない場合がある。 - そういうときはラップして使ってね
Context
- ContextはClient Componentでしか使えない
- しかし、Global Stateを管理するContextは多くの場合ルート付近に位置させたい
- じゃあどうするの?
- Context ProviderだけのClient Component、たとえば
providers.tsx
を作って、Layoutでimportしてchildrenをラップすれば良い。 - Providerが必要なサードパーティ製のライブラリも同じやり方でできる
静的レンダリングと動的レンダリング
- 静的レンダリング:SSG、ISRに対応する。ビルド時にサーバー上でレンダリングされ、結果がキャッシュされる。パフォーマンスが向上
- 動的レンダリング:SSRに相当。リクエスト毎にサーバー上でレンダリングされる。
- デフォルトは静的。fetch関数のオプションでキャッシュの有無を設定して動的に切り替えるほか、動的関数を使用していると動的レンダリングになる。
- 動的関数とは:ユーザーのクッキー、現在のリクエストヘッダ、URLの検索パラメータなど、リクエスト時にしか知り得ない情報に依存する関数
Data Fetching
- やっと来た😭
-
getServerSideProps
、getStaticProps
、getInitialProps
はなくなりました - 基本的にはServer ComponentでData Fetchする
- Client ComponentでもSWRやReact Queryを使ってData Fetchできる。将来的には、Reactの
use()
を使ってData Fetchする - コンポーネントレベルのデータフェッチができるようになった
- 2つのデータ取得パターン、Parallel と Sequential
- Parallel:同時にリクエストを行う。ロード時間を短縮できる
- Sequential:リクエストが依存し合い、ウォーターフォールを形成する
- 複数のコンポーネントで同じリクエストをしても、まとめて1つのリクエストにしてくれる。
-
fetch()
関数を使わない場合、手動でキャッシングができるらしい。
-
- 2つのデータのタイプ:静的データと動的データ
- 静的データ:頻繁に変化しないデータ。ブログの記事など。
- 動的データ:頻繁に変化したり、ユーザーに特定される可能性のあるデータ。ショッピングカートのリストなど。
- デフォルトでの静的フェッチは、データをビルド時にキャッシュし、リクエストではキャッシュを再利用する。
fetch('https://...');
-
fetch('https://...', { next: { revalidate: 10 } });
:キャッシュの寿命を指定
- 常に最新のデータをフェッチしたい場合は、キャッシュせずにリクエストごとにデータをフェッチできる。
fetch('https://...', { cache: 'no-store' });
- page.tsxにて、データ取得関数でerrorをthrowすれば、直近のerror.jsが表示される
- Client ComponentでData Fetchする場合、将来的には
use()
が使えるが、今のところはSWRや React Queryが推奨されている - 従来のpagesディレクトリでのサーバーサイドのデータ取得を使うには、ページ全体のロードを待つ必要があった。
- appディレクトリでは、必要な部分だけロードすることができる
-
fetch()
を使用せずORMなどを使う場合でも、レイアウトやページのキャッシュや再検証の動作を制御できる。 - デフォルトの動作
- セグメントが静的な場合:ビルド時にキャッシュされ?、リクエストではキャッシュが再利用される?。
- セグメントが動的な場合:リクエスト毎にフェッチされる
キャッシュ
- Next.jsの2つのキャッシュ方法:セグメントレベルとリクエスト単位
セグメントレベルのキャッシング
- page.tsxとlayout.tsxでrevalidate値をexportする
export const revalidate = 60;
リクエスト単位のキャッシング
- リクエストの重複排除ができる
fetch()
関数を使わなくても重複排除は実現できる- それが、ラップされた関数の結果をメモする新しい関数
cache()
- 同じ引数で呼び出された同じ関数は、関数を再実行する代わりに、キャッシュされた値を再利用できる!!
import { cache } from 'react';
export const getUser = cache(async (id: string) => {
const user = await db.user.findUnique({ id });
return user;
});
こんな感じで定義すれば、getUser(1)を複数のコンポーネントで何回呼び出してもデータベースアクセスは1回で済むのじゃああああ★★★
- これにより、同じデータが複数回必要な場合でも、単に毎回直接取得関数を呼び出せばよくなる。
- つまり、propsのバケツリレーがなくなる☆☆☆☆☆☆
-
server-only
パッケージの使用を推奨
Preload、どうして必要なのかよくわからぬ・・
並列データフェッチの上にさらに最適化されたものらしいけど。合わせ技
cache、preloadパターン、 server-onlyパッケージを組み合わせて、アプリ全体で使えるデータ取得ユーティリティを作成することができる!
import { cache } from 'react';
import 'server-only';
export const preload = (id: string) => {
void getUser(id);
}
export const getUser = cache(async (id: string) => {
// ...
});
天才では?
でも、キャッシュを再検証したい場合どうするの?
たとえばgetUser()したあとにユーザー情報を変更して、その後またユーザー情報を表示しても前のままだったら困るでしょ
ということで
Revalidating Data
- 2つの方法
- Background: 一定時間ごとに再検証を行う
- On-demand: 更新などのイベントで再検証を行う
Background Revalidation
-
fetch()
を使う場合- オプションで指定
fetch('https://...', { next: { revalidate: 60 } });
- 使わない場合(ORMなど)
- page.tsxまたはlayout.tsxでrevalidateをexport
-
export const revalidate = 60;
- これはトップレベルで指定するの?それとも個々のコンポーネントで???
On-demand Revalidation
ぬぬぬぬ?
ふ、ふぬ・・?
Mutating Data
- データが変更されたら、
router.refresh()
を使ってルートレイアウトから下の現在のルートを更新できる。router.refresh()
はサーバーに新しいリクエストを行い、データリクエストを再取得してServer Componentsを再レンダリングするらしい! - これを知りたかった
PUTリクエストをしてrefreshするサンプルコードがuseTransitionの用例含めとてもわかりやすかったので長いけど貼っておく
mutating data
import Todo from './todo';
interface Todo {
id: number;
title: string;
completed: boolean;
}
async function getTodos() {
const res = await fetch('https://api.example.com/todos', {
cache: 'no-store',
});
const todos: Todo[] = await res.json();
return todos;
}
export default async function Page() {
const todos = await getTodos();
return (
<ul>
{todos.map((todo) => (
<Todo key={todo.id} {...todo} />
))}
</ul>
);
}
"use client";
import { useRouter } from 'next/navigation';
import { useState, useTransition } from 'react';
interface Todo {
id: number;
title: string;
completed: boolean;
}
export default function Todo(todo: Todo) {
const router = useRouter();
const [isPending, startTransition] = useTransition();
const [isFetching, setIsFetching] = useState(false);
// Create inline loading UI
const isMutating = isFetching || isPending;
async function handleChange() {
setIsFetching(true);
// Mutate external data source
await fetch(`https://api.example.com/todo/${todo.id}`, {
method: 'PUT',
body: JSON.stringify({ completed: !todo.completed }),
});
setIsFetching(false);
startTransition(() => {
// Refresh the current route:
// - Makes a new request to the server for the route
// - Re-fetches data requests and re-renders Server Components
// - Sends the updated React Server Component payload to the client
// - The client merges the payload without losing unaffected
// client-side React state or browser state
router.refresh();
// Note: If fetch requests are cached, the updated data will
// produce the same result.
});
}
return (
<li style={{ opacity: !isMutating ? 1 : 0.7 }}>
<input
type="checkbox"
checked={todo.completed}
onChange={handleChange}
disabled={isPending}
/>
{todo.title}
</li>
);
}
Streaming and Suspense
- ストリーミングとは
- 従来のレンダリングではサーバー側でデータフェッチを行ってからページの描画が行われるので、データフェッチが終わるまでページ全体が表示されなかった
- ストリーミングでは、個々のコンポーネントで段階的にレンダリングが行われ、データに依存しないUIはいち早く描画され、データフェッチが必要なUIはあとから表示されるので、UXが向上する
- Next.jsでストリーミングを実装するには、loading.js(ルートセグメント全体)またはSuspense boundaries(より細かい制御)でできる
loading.js
- 先述のように、セグメント単位でloadingを表示するにはloading.jsを使う。
Suspense Boundaries
- 独自のコンポーネントをSuspense Boundaryでラップする。
- <Suspense>は、非同期アクション(例:データの取得)を実行するコンポーネントをラップし、アクションが発生している間は予備UI(例:スケルトン、スピナー)を表示し、アクションが完了したらあなたのコンポーネントに入れ替えることで機能します。
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>
);
}
Image Generation
route.tsxで簡単に画像生成できる
読んでたらだいたい疑問は解決した。
気になるのはRoute Handlerまわり?🤔あまり解説がなかったような・・。
とりあえず色々いじってみます。
わ!App RouterがStableになった!!🤯🎉🎉🎉
ドキュメントもbetaが外れたみたいなのでもう1回ざっと読み直します😇
Parallel Routes, Intercepting Routesがかなりわかりやすくなってる。