Open15
Next.js(App Router)のお勉強
Next.js(App Router)の優位性
- パフォーマンス
- 開発者体験
- 多様性
Route定義に関わる用語
Tree / Subtree
- Tree:階層構造を示す規則
- Subtree:任意のノードをRootとして始め、Leafで終わるTree
- Root:TreeまたはSubtreeにおける最初のノード
- Leaf:子のない最後のノード
Segment / Path
- Segment:スラッシュで区切られたURLの一部
- Path:ドメインの後に続くURL文字列
Route Segment
- Route Sement:特定のPathに対応するSegment
- Root Segment:一番上のSegment
- Leaf Segment:子Segmentを持たないSegment
Dynamic Route / Dynamic Segment
- Dynamic Route:Pathが動的に変わりうるもの、URLパスパラメーターを参照する前提のRoute
- Dynamic Segment:Dynamic Routeを構成する[]が含まれたSegment
Server Component / Client Component
- Server Component
- サーバーサイドでのみ実行されるコンポーネント
- 非同期関数として書くことができる
- ブラウザで実行すべきJavaScriptは送られない
- Client Component
- ブラウザ/サーバー両方で実行されるコンポーネント
- Client Componentからimportされるコンポーネントや関連ファイルもブラウザ向けにバンドルされる
Server Component / Client Componentの使い分け
- Server Component
- データを取得する
- バックエンドリソースを取得する
- 機密情報を扱う
- Client Component
- インタラクティブな機能をもつ
- コンポーネントに保持した状態を扱う
- ブラウザ専用のAPIを使用する
- ブラウザ専用のHooksを使用する
- React Classコンポーネントを使用する
動的データ取得と静的データ取得
- 静的データは、更新頻度が低く、誰もが共有できるデータ
- fetch関数では、指定がなければ、静的データ取得と扱われて結果がキャッシュされる
- 動的データとして取得したい場合は、
{ cache: "no-store" }
を指定する
Routeのレンダリング
- Routeごとのレンダリング種別
- 静的レンダリングRoute:すべてのリクエストに対して、同一のレンダリング結果をレスポンスする
- 動的レンダリングRoute:リクエストの内容に応じて、異なるレンダリング結果をレスポンスする
-
動的レンダリングになる要因
- 動的データ取得の使用:
{ cache: "no-store"}
を指定したfetch関数 - 動的関数の使用
- Cookieの参照
- リクエストヘッダーの参照
- URL検索パラメーターの参照
- Dynamic Segmentの使用
- 動的データ取得の使用:
Segment構成ファイル
- React Suspense:子が読み込みを完了するまでフォールバックUIを表示する機能
- Error Boundary:子コンポーネントでErrorがスローされた場合にフォールバックUIを表示する実装
Routeのメタデータ
- 静的メタデータ:固定のメタデータを出力。metadataオブジェクトを任意のPageもしくはLayoutからexportする
export const metadata = {
title: SITE_NAME,
description:
"「Photo Share」は、ユーザーが自由に写真を共有し、コメントや「いいね」を通じて交流することができるSNSアプリケーションです。",
};
- 動的メタデータ:リクエスト内容に応じてメタデータを出力。generateMetadata関数をexportする
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const photo = await getPhoto(params.photoId);
return {
title: photo.title,
description: photo.description,
};
}
- メタデータは親から継承することができる
Route Handler
- Route Handlerの定義には、Segment構成フォルダに
route.ts
というファイルを配置する -
route.ts
からexportする関数は、HTTPリクエストのメソッドに対応する- GET、POST、PUT、PATCH、DELETE、HEAD、OPTIONS
- 対応する関数がない場合、405 Method Not Allowedレスポンスが返される
- コンフリクトが起こる可能性があるので、
/api
フォルダを用意しておくと良い - Route HandlerはJSONをレンダリングする
- ブラウザからのHTTPリクエストはシンプルに、認証認可などをNext.js自身で処理することができる
静的Route Handler
- レスポンスボティ(JSON)をあらかじめキャッシュファイルとして出力する
動的Route Handler
- 以下の要因が検出されると動的Route Handlerとみなす
- Dynamic Segmtn値の参照
- Requestオブジェクトの参照
- 動的関数の使用
- GETとHEAD以外の HTTP関数のexport
- Segment Config Optionsの指定
fetch関数でのデータ取得
export const host = process.env.API_HOST;
export const path = (path?: string) => `${host}${path}`;
export class FetchError extends Error {
status: number;
constructor(message: string, status: number) {
super(message);
this.status = status;
}
}
export const handleSucceed = async (res: Response) => {
const data = await res.json();
if (!res.ok) {
throw new FetchError(res.statusText, res.status);
}
return data;
};
export const handleFailed = async (err: unknown) => {
if (err instanceof FetchError) {
console.warn(err.message);
}
throw err;
};
- 1回のレンダリングで同じデータ取得を行いたい場合は、共有関数にまとめると良い
- 「Incremental Cache」はデータ取得結果をキャッシュし、必要に応じて更新するメカニズムを持つ(拡張されたfetch関数によるDataキャッシュ)
- 拡張されたfetch関数は何もしてされない限り、取得したデータを1年間キャッシュする
- Time-based Revalidation:next.revalidateオプションによる有効期間で指定する
- fetch関数自体に有効期間を指定すると、キャッシュを活用しWeb APIサーバーへのアクセス頻度を削減できる
cache
コールバックの紐付けが上手いカスタムフック
import { useEffect, useRef } from "react";
export function useKey(code: string, cb: (event: KeyboardEvent) => void) {
const cbRef = useRef(cb);
cbRef.current = cb;
useEffect(() => {
const handleKeyPress = (event: KeyboardEvent) => {
if (event.code === code) {
cbRef.current(event);
}
};
window.addEventListener("keydown", handleKeyPress);
return () => {
window.removeEventListener("keydown", handleKeyPress);
};
}, [code]);
}
fetchのプロミスを処理周りが上手いなーという
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!photoData) return;
try {
// 【3】アップロードした「写真 URL」を取得(A)
const imageUrl = await uploadPhoto({ photoData });
// 【4】投稿内容と「写真 URL」をまとめて Route Handler に送る
const { photo } = await fetch("/api/photos", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
imageUrl,
title,
categoryId,
description,
}),
}).then((res) => {
if (!res.ok) throw new Error();
return res.json();
});
router.refresh();
router.push(`/photos/${photo.id}`);
} catch (err) {
window.alert("写真のアップロードに失敗しました");
}
close();
};
childrenは関数でも良いのか
import { useCallback, useState } from "react";
import { clsx } from "clsx";
import { useDropzone } from "react-dropzone";
import { getImageElementFromFile, resizePhoto } from "./fns";
type Props = {
className?: string;
areaClassName?: string;
dragActiveClassName?: string;
maxUploadRectSize: number;
maxUploadFileSize: number;
children?: (isDragActive: boolean) => React.ReactNode;
onChange: (file: Blob) => void;
};
export function PhotoDndUploader({
className,
areaClassName,
dragActiveClassName,
maxUploadFileSize,
maxUploadRectSize,
children,
onChange,
}: Props) {
const [imgSrc, setImgSrc] = useState<string>();
const onDrop = useCallback(
async (acceptedFiles: File[]) => {
const file = acceptedFiles[0];
const image = await getImageElementFromFile(file);
const resizedFile = await resizePhoto({
image,
size: maxUploadRectSize,
});
setImgSrc(image.src);
onChange?.(resizedFile);
},
[onChange],
);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: { "image/jpeg": [".jpeg", ".jpg"] },
maxSize: maxUploadFileSize,
maxFiles: 1,
});
return (
<div className={className}>
<div
{...getRootProps()}
className={clsx(areaClassName, isDragActive && dragActiveClassName)}
{...(imgSrc && { style: { backgroundImage: `url(${imgSrc})` } })}
>
<input {...getInputProps()} />
{!imgSrc && <>{children?.(isDragActive)}</>}
</div>
</div>
);
}
クライアント、Route Handler、バックエンドの責務分離がミソポイントか...?
useFormStateの上手い使い方
export function LikeButtonForm({ photo, isOwner, liked }: Props) {
// 【1】「いいね」総数が何件か「いいね」済みか否かを、初期値として保持
const [state, dispatch] = useFormState(
postLike,
initialFormState({ liked, likedCount: photo.likedCount }),
);
return (
<form action={dispatch}>
<input type="hidden" name="photoId" value={photo.id} />
<LikeButtonComponent
likedCount={state.likedCount} // 【3】送信に成功した場合「いいね」数が増える
disabled={isOwner || Boolean(state.liked)}
/>
{/* 【4】送信に失敗した場合、AlertDialog を表示 */}
{state.error && (
<AlertDialogModalComponent
key={state.updatedAt} // ★ エラーが発生するごとに再マウント、内部状態が破棄される
status={state.error.status}
/>
)}
</form>
);
- Page Routerでは、サーバーサイドのデータを取得できる場所が各画面のRoutewごとに1箇所に限定されていた。
getStaticProps
、getServerSideProps
などで取得したデータを子に渡していた - App Routerでは、「コロケーション」を活用して、必要なデータはコンポーネント自身で取得することができる