Open28

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/を作成。

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    appDir: true,
  },
};

module.exports = nextConfig;

Root Layoutを作る

app/layout.tsxを作る。すべてのページに適用される。
ここに。<html>と<body>を定義する。

app/layout.tsx
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は使わず以下のようにする

page.tsx
export const metadata = {
  title: 'Home',
  description: 'Welcome to Next.js',
};

metadataではogpやfaviconなど設定できる
https://beta.nextjs.org/docs/api-reference/metadata
metadataの型は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でもサポートされているらしい
  • ひょっとしてデータフェッチのほとんどのケースではSSRで事足りるのでは?

  • Server Componentでデータ取得してるローディング中にloading.tsxを表示できるってことなのかな??まだそこまで行ってないから分からないけど。

  • どこをSSRしてどこをCSRすればいいのかな?ややこしい。。

  • 基本的にはSSRでデータ取得しちゃえばいいのじゃろうか??

  • POST, UPDATE, DELETEについては、従来通りクライアント側からAPIリクエストすればいいのかな?

わからん。。
つぎはここからよむ
https://beta.nextjs.org/docs/upgrade-guide#step-6-migrating-data-fetching-methods

こるりりこるりり

データフェッチ

SSR

従来のgetServerSideProps
fetchのオプションで{ cache: 'no-store' }を指定することで実現する

page.tsx
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();

新しい関数cookies()headers()を使って取得する。

SSG

従来のgetStaticProps
fetch関数はデフォルトでcache: 'force-cache'になっているので、fetch関数をそのまま使用すれば良い。

page.tsx
const res = await fetch(`https://...`);
const projects = await res.json();

Dynamic paths (getStaticPaths)

getStaticPathsgenerateStaticParamsになりました。

ISR

{ next: { revalidate: 60 } }オプションをfetch関数に入れる。

API Routes

app/api/ではRoute Handlersというのを使えるらしい。
なんと!

app/api/route.ts
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はレンダリングに関係ないけど)
ここの図がわかりやすい:
https://beta.nextjs.org/docs/routing/fundamentals#component-hierarchy
なるほど〜
良い感じですね。

ディレクトリ内には普通にコンポーネントやCSS、storybookなど他のファイルも自由に置ける。

ページレベルじゃない小さなコンポーネントでもSuspenseを使いたいときは、普通にSuspenseを直書きする
https://react.dev/reference/react/Suspense

  • 共通のレイアウトがあるページ間の移動では、変更がある部分だけいい感じに部分レンダリングが行われるらしい。
こるりりこるりり

Defining Routes

page.jsを置かなければパスは通らないので、page.jsのないディレクトリを作ってコンポーネントとか置ける!。

Route Groups

通常はディレクトリ構造がそのままパスにマッピングされるが、Route Groupsを使えばパスに影響しない形でルートを整理できる。
ルートレイアウトを複数作りたいときとかにも使える。(複数のルートレイアウト間を移動すると部分レンダリングではなくフルページロードされる)
Route Groupの作り方は、フォルダ名を括弧で囲む(shop)/みたいなかんじ

Dynamic Segments

フォルダ名を角括弧で囲む。 [slug]/みたいなかんじ
その中身はparamspropsに渡されます。
pagesのDynamic Routesと同じ

  • [...folderName]/とすると、それ以降のsegmentを配列で受け取れる。
  • shop/[[...folderName]]/とすると、それ以降のsegmentを配列で受け取れる上に、そのsegmentが指定されていなくてもshop/でもアクセスできるようになる

Dynamic Segmentsに型を付ける場合はこう

page.tsx
 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の例
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

待ってました🎉

app/api/items/route.ts
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(条件付きルーティング)の実装に使える。

使うときになったら読もう
https://beta.nextjs.org/docs/routing/parallel-routes

Intercepting Routes

サイドバーでカートを開いたり、モーダルでページを開いたりして、リロードしたりすると単一のページとして表示されるようなルーティングができる。
これも実装するときになったら読もう
https://beta.nextjs.org/docs/routing/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

  • やっと来た😭
  • getServerSidePropsgetStaticPropsgetInitialPropsはなくなりました
  • 基本的にはServer ComponentでData Fetchする
  • Client ComponentでもSWRやReact Queryを使ってData Fetchできる。将来的には、Reactのuse()を使ってData Fetchする
  • コンポーネントレベルのデータフェッチができるようになった
  • 2つのデータ取得パターン、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()
  • 同じ引数で呼び出された同じ関数は、関数を再実行する代わりに、キャッシュされた値を再利用できる!!
utils/getUser.ts
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、どうして必要なのかよくわからぬ・・
https://beta.nextjs.org/docs/data-fetching/caching#preload-pattern-with-cache
並列データフェッチの上にさらに最適化されたものらしいけど。


合わせ技

cache、preloadパターン、 server-onlyパッケージを組み合わせて、アプリ全体で使えるデータ取得ユーティリティを作成することができる!

utils/getUser.ts
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

https://beta.nextjs.org/docs/data-fetching/revalidating#using-on-demand-revalidation
なんかとても便利そうだけど
ぬぬぬぬ?
ふ、ふぬ・・?

こるりりこるりり

Mutating Data

  • データが変更されたら、router.refresh()を使ってルートレイアウトから下の現在のルートを更新できる。router.refresh()サーバーに新しいリクエストを行い、データリクエストを再取得してServer Componentsを再レンダリングするらしい!
  • これを知りたかった

https://beta.nextjs.org/docs/data-fetching/mutating
PUTリクエストをしてrefreshするサンプルコードがuseTransitionの用例含めとてもわかりやすかったので長いけど貼っておく

mutating data
app/page.tsx
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>
  );
}
app/todo.tsx
"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のサンプル
app/dashboard/page.tsx
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>
  );
}
こるりりこるりり

読んでたらだいたい疑問は解決した。
気になるのはRoute Handlerまわり?🤔あまり解説がなかったような・・。
とりあえず色々いじってみます。

こるりりこるりり

わ!App RouterがStableになった!!🤯🎉🎉🎉
ドキュメントもbetaが外れたみたいなのでもう1回ざっと読み直します😇