🐈

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を状態として使う


なにが起きているか?

  1. URLが変わる
  2. サーバーコンポーネントが再実行される
  3. 最新データを取得

👉 これだけ


なぜこれで楽になるのか?

■ 状態管理が激減

  • Redux不要
  • Contextの肥大化防止

■ useEffectが減る

👉 データ取得のためのuseEffectはほぼ不要


■ バグが減る

  • 同期ズレが起きない
  • 状態の出どころが明確

ただし注意点

ここは誤解されやすいポイントです。

■ Contextは不要ではない

以下は普通に使います:

  • 認証状態
  • テーマ
  • モーダル制御

👉 「必要なときだけ使う」が正解


アンチパターン

❌ Clientでデータ取得

useEffect(() => {
  fetch('/api');
}, []);

👉 不要な通信・遅延の原因


❌ 状態を持ちすぎる

const [data, setData] = useState([]);

👉 サーバーで取ればいい


❌ 分割しすぎ

atoms/
molecules/
organisms/

👉 管理コスト増加


まとめ

  • Server Firstで考える
  • Clientは最小限にする
  • Feature単位で分ける
  • 状態管理は減らす

おわりに

設計で迷ったときは、この一問だけ考えてください。

👉 「この処理は本当にブラウザでやる必要があるか?」

この視点を持つだけで、設計は一気にシンプルになります。

Discussion