Next.js App Router(RSC 前提)における最小かつ実務的な初期設計を示すベストプラクティス集です。
- App Router を「使っている」ではなく思想として理解して使う
- CSR / Pages Router 脳から脱却した設計を示す
- RSC 前提で責務の置き場所の判断基準を明確にする
テンプレート集でも、フルスタック雛形でもありません。 設計判断をコード構造で示すためのリポジトリです。
pnpm install
pnpm devhttp://localhost:3000 でアプリケーションが起動します。
- Server Component がデフォルト
- Client Component は最後の手段
- データ取得はサーバーで完結
- useEffect は DOM が必要な場合のみ
| 責務 | 配置 |
|---|---|
| データ取得 | Server Component |
| ビジネスロジック | lib/ |
| 副作用(CRUD) | Server Actions |
| UI 状態・操作 | Client Component |
Before(CSR / useEffect)
'use client'
export default function RobotList() {
const [robots, setRobots] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('/api/robots')
.then(res => res.json())
.then(data => {
setRobots(data)
setLoading(false)
})
}, [])
if (loading) return <p>Loading...</p>
return <ul>{robots.map(r => <li key={r.id}>{r.name}</li>)}</ul>
}After(RSC)
// Server Component - 'use client' なし
import { getRobots } from '@/lib/robots'
export default async function RobotList() {
const robots = await getRobots()
return <ul>{robots.map(r => <li key={r.id}>{r.name}</li>)}</ul>
}Before(CSR / useState + fetch)
'use client'
export default function CreateRobotForm() {
const [name, setName] = useState('')
const [submitting, setSubmitting] = useState(false)
const handleSubmit = async (e) => {
e.preventDefault()
setSubmitting(true)
await fetch('/api/robots', { method: 'POST', body: JSON.stringify({ name }) })
setSubmitting(false)
}
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={e => setName(e.target.value)} />
<button disabled={submitting}>Create</button>
</form>
)
}After(Server Actions + useActionState)
'use client'
import { useActionState } from 'react'
import { createRobot } from './_actions/robot'
export default function CreateRobotForm() {
const [state, action, pending] = useActionState(createRobot, null)
return (
<form action={action}>
<input name="name" />
<button disabled={pending}>Create</button>
{state?.error && <p>{state.error}</p>}
</form>
)
}src/
├─ app/
│ ├─ layout.tsx # ルートレイアウト(Server Component)
│ ├─ page.tsx # トップページ(Server Component)
│ ├─ auth/
│ │ ├─ page.tsx # 認証ページ(Server Component)
│ │ └─ _actions/ # 認証 Server Actions
│ ├─ robots/
│ │ ├─ page.tsx # 一覧ページ(Server Component)
│ │ ├─ loading.tsx # ローディング UI
│ │ ├─ [id]/
│ │ │ ├─ page.tsx # 詳細ページ(Server Component)
│ │ │ └─ loading.tsx # ローディング UI
│ │ ├─ _actions/ # Server Actions
│ │ │ ├─ robot.ts
│ │ │ └─ robot.test.ts # テスト
│ │ └─ _components/ # ページ固有コンポーネント
│ └─ robots-paginated/ # ページネーション例
├─ components/ # 共有コンポーネント
│ ├─ Header.tsx # Server Component
│ ├─ Pagination.tsx # Server Component
│ └─ LogoutButton.client.tsx # Client Component
└─ lib/ # ビジネスロジック
├─ robots.ts
├─ robots.test.ts
├─ auth.ts
└─ auth.test.ts
*.client.tsx- Client Component(明示的に区別)_actions/- Server Actions(private)_components/- ページ固有コンポーネント(private)loading.tsx- ローディング UI(自動的に Suspense boundary として機能)
/robots- 一覧表示 + 新規作成/robots/[id]- 詳細 + 編集
ポイント:
- Server Component でデータ取得
- Server Actions で CRUD
- useActionState でフォーム状態管理
- loading.tsx でスケルトン UI
/auth- ログインページ
ポイント:
- Server Component で認証チェック
- 認証済みならリダイレクト
- cookies() で セッション管理
/robots-paginated- URL パラメータでページ管理
ポイント:
- searchParams で状態管理
- useState ではなく URL で状態を持つ
- SEO フレンドリー
# 全テスト実行
pnpm test
# 個別実行
pnpm test run src/lib/robots.test.ts
pnpm test run src/app/robots/_actions/robot.test.tsテストファイルは本体と同じ階層に配置しています。
- Pages Router 互換の設計
- CSR 前提のデータ取得(useEffect + fetch)
- API Routes を UI 専用に使う構成
- グローバルな状態管理ライブラリの導入
- hooks ディレクトリの乱立
MIT