🐈
Next.js App Router時代の設計論:Feature-Based × Server Firstで状態管理を最小化する
はじめに
最近、フロントエンドの設計でこんな悩みを感じていませんか?
- useEffectが増えすぎて、どこでデータ取得しているか分からない
- ContextやReduxが肥大化している
- コンポーネントの分け方(Atomic Design)に迷う
これらの問題は、実は設計思想の変化によって解決できます。
Next.js のApp Router(React Server Components)では、これまでの常識が大きく変わりました。
なにが変わったのか?
従来の設計では、
👉 「コンポーネントをどう分割するか(粒度)」
が重要でした。
しかし現在は、
👉 「その処理はどこで実行されるのか?」
つまり
- サーバーでやるべきか
- クライアントでやるべきか
が最も重要になっています。
コンセプト①:Server First
まず基本となる考え方です。
■ サーバーでやること
- データ取得(API通信)
- 認証チェック
- 重いロジック
これらはすべてサーバー側で実行します。
■ なぜか?
理由はシンプルです。
- ブラウザに送るJavaScriptが減る → 高速化
- APIキーやトークンが漏れない → セキュリティ向上
- 状態管理が減る → バグ減少
コンセプト②:Islands of Interactivity
とはいえ、すべてをサーバーでやることはできません。
例えば:
- 入力フォーム
- モーダル
- ボタン操作
こういった「ユーザーの操作」はブラウザで処理する必要があります。
そこで使うのが👇
👉 「必要な部分だけClientにする」
という考え方です。
イメージ
- サーバーコンポーネント → 海
- クライアントコンポーネント → 島
👉 必要なところだけ「島」として置く
ディレクトリ構成(Feature-Based設計)
ここで重要なのが、フォルダ構成です。
src/
├── app/
│ └── diary/
│ └── page.tsx
│
├── features/
│ └── diary/
│ ├── api/
│ ├── hooks/
│ ├── types/
│ └── components/
│ ├── DiaryContainer.tsx
│ ├── DiaryList.tsx
│ ├── DiarySearch.tsx
│ └── DiaryRowActions.tsx
│
└── components/
└── ui/
■ なぜこの構成がいいのか?
「日記機能に関係するもの」をすべて features/diary に閉じ込めています。
これにより:
- 他機能に影響しない
- チーム開発で衝突しにくい
- 修正箇所がすぐ分かる
実装の流れ(重要)
ここからが一番大事です。
① ページは「つなぐだけ」
import { DiaryContainer } from '@/features/diary/components/DiaryContainer';
export default async function DiaryPage({ searchParams }: {
searchParams: { q?: string }
}) {
const q = searchParams.q ?? '';
return (
<DiaryContainer query={q} />
);
}
👉 ページでロジックを書かないのがポイント
② データ取得はServerコンポーネントで
export async function DiaryContainer({ query }: { query: string }) {
const diaries = await getDiaries(query);
return (
<>
<DiarySearch initialQuery={query} />
<DiaryList diaries={diaries} />
</>
);
}
👉 ここでAPIを叩く
③ Clientは「操作だけ」
'use client';
export function DiarySearch({ initialQuery }) {
const router = useRouter();
const handleSearch = (term) => {
const params = new URLSearchParams(window.location.search);
term ? params.set('q', term) : params.delete('q');
router.push(`?${params.toString()}`);
};
return <Input defaultValue={initialQuery} onChange={(e) => handleSearch(e.target.value)} />;
}
ここが一番重要:状態をどこで持つか?
従来:
useState + useEffect
現在:
👉 URLを状態として使う
なにが起きているか?
- URLが変わる
- サーバーコンポーネントが再実行される
- 最新データを取得
👉 これだけ
なぜこれで楽になるのか?
■ 状態管理が激減
- Redux不要
- Contextの肥大化防止
■ useEffectが減る
👉 データ取得のためのuseEffectはほぼ不要
■ バグが減る
- 同期ズレが起きない
- 状態の出どころが明確
ただし注意点
ここは誤解されやすいポイントです。
■ Contextは不要ではない
以下は普通に使います:
- 認証状態
- テーマ
- モーダル制御
👉 「必要なときだけ使う」が正解
アンチパターン
❌ Clientでデータ取得
useEffect(() => {
fetch('/api');
}, []);
👉 不要な通信・遅延の原因
❌ 状態を持ちすぎる
const [data, setData] = useState([]);
👉 サーバーで取ればいい
❌ 分割しすぎ
atoms/
molecules/
organisms/
👉 管理コスト増加
まとめ
- Server Firstで考える
- Clientは最小限にする
- Feature単位で分ける
- 状態管理は減らす
おわりに
設計で迷ったときは、この一問だけ考えてください。
👉 「この処理は本当にブラウザでやる必要があるか?」
この視点を持つだけで、設計は一気にシンプルになります。
Discussion