React / Next.js (App Router) のコードを書く・レビューする際に、ベストプラクティスに沿っているか確認し、改善提案を行う。コンポーネント設計、状態管理、パフォーマンス最適化、アクセシビリティなどを網羅。
このプロジェクト(Next.js App Router + TypeScript + yamada-ui)における React のベストプラクティス。
"use client" を付与する。"use client" が必要なケース: useState, useEffect, useRef, イベントハンドラ (onClick 等), ブラウザ API, yamada-ui のインタラクティブコンポーネント。"use client" にせず、インタラクティブな部分だけを小さな Client Component に切り出す。// Good: インタラクティブ部分だけ Client Component
// app/dashboard/page.tsx (Server Component)
import { InteractiveChart } from "./interactive-chart";
export default async function DashboardPage() {
const data = await fetchData(); // サーバーで取得
return (
<div>
<h1>Dashboard</h1>
<InteractiveChart data={data} />
</div>
);
}
any は禁止。isDisabled ではなく disabled、ただし yamada-ui の API に従う場合は除く)。// Good
type ChartCardProps = {
title: string;
data: DataPoint[];
children?: React.ReactNode;
};
// Bad
type ChartCardProps = {
title: string;
data: any;
isNotHidden?: boolean;
};
useState) — UI のトグル、フォーム入力など// Bad
const [items, setItems] = useState<Item[]>([]);
const [count, setCount] = useState(0);
// items が変わるたびに setCount(items.length) を呼ぶ…
// Good
const [items, setItems] = useState<Item[]>([]);
const count = items.length; // 派生値
useReducer の活用useReducer を検討する。useMemo,useCallback は基本的に不要です.react compiler がついています.
key には安定した一意の ID を使う。配列の index は並べ替え・追加・削除がない場合のみ許容。// Good
{
users.map((user) => <UserCard key={user.id} user={user} />);
}
// Bad
{
users.map((user, index) => <UserCard key={index} user={user} />);
}
next/image を使用する。width, height を必ず指定する。next/dynamic で遅延ロードする。import dynamic from "next/dynamic";
const HeavyChart = dynamic(() => import("./heavy-chart"), {
loading: () => <Skeleton height="300px" />,
});
fetch や DB クエリは Server Component 内で直接行う。async/await をそのまま使える。// app/dashboard/page.tsx
export default async function DashboardPage() {
const stats = await db.select().from(sessions);
return <StatsDisplay stats={stats} />;
}
"use server" ディレクティブを関数またはファイルの先頭に記述する。"use server";
import { z } from "zod";
const schema = z.object({
name: z.string().min(1),
});
export async function createItem(formData: FormData) {
const parsed = schema.safeParse({
name: formData.get("name"),
});
if (!parsed.success) {
return { error: parsed.error.flatten() };
}
// DB 操作
}
loading.tsx と error.tsx を配置して、Suspense / Error Boundary を活用する。use プレフィックス付きのカスタム Hook に切り出す。function useSessionStats(userId: string) {
const [stats, setStats] = useState<Stats | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// fetch logic
}, [userId]);
return { stats, isLoading };
}
exhaustive-deps ルールに従う。React.FC は使わない。// Good
function UserCard({ name, score }: { name: string; score: number }) {
return (
<div>
{name}: {score}
</div>
);
}
// Good (型が複雑な場合)
type UserCardProps = {
name: string;
score: number;
onSelect?: (id: string) => void;
};
function UserCard({ name, score, onSelect }: UserCardProps) {
return (
<div>
{name}: {score}
</div>
);
}
React.MouseEvent<HTMLButtonElement> など具体的な型を使う。as による型アサーションや ! (non-null assertion) は原則禁止。型ガードや zod でのパースで安全に絞り込む。<button>, <nav>, <main> 等)。<div onClick> は禁止。alt テキストを設定する。装飾画像は alt="" にする。<label> を関連付ける。yamada-ui の FormControl を使う。error.tsx) でクラッシュを防ぐ。// app/dashboard/error.tsx
"use client";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>エラーが発生しました</h2>
<button onClick={reset}>再試行</button>
</div>
);
}
stats-card.tsx)StatsCard)use prefix (useSessionStats)SessionStats)