Reactアンチパターン10選|保守性が下がる設計と改善例を解説

Reactアンチパターンと保守性が下がるコンポーネント設計の改善例

保守性が下がるReactアンチパターンを解説

Reactアンチパターンを知っておくと、実務で保守しにくいコードを避けやすくなります。

そのため、Reactは自由度が高いライブラリである一方、設計の差が出やすい技術でもあります。同じ画面でも、コンポーネントの分け方、stateの置き場所、useEffectの使い方によって、読みやすさや修正しやすさが大きく変わります。

学習段階では「動けばOK」でも、実務ではそれだけでは不十分です。機能追加、バグ修正、引き継ぎ、リファクタリングが続くため、後から変更しやすい設計が重要になります。

この記事では、React初心者や実務経験の浅いエンジニア向けに、保守性が下がるReactアンチパターンを10個紹介します。悪い例と改善例を見ながら、React・Next.js案件で意識したい設計の考え方を整理します。

Reactアンチパターンを避けるとなぜ保守性が上がるのか

まず、Reactにおける保守性とは、簡単に言うと後から変更しやすいことです。

例えば、1つのコンポーネントにAPI取得、入力管理、バリデーション、一覧表示、モーダル制御をすべて書くと、最初は動いても、後から修正するときに影響範囲が読みにくくなります。

保守性が高いReactコード

読みやすい
↓
変更しやすい
↓
テストしやすい
↓
引き継ぎやすい

一方で、Reactアンチパターンが積み重なると、どこを直せばよいかわからない、バグが入りやすい、レビューに時間がかかる、という状態になります。つまり、Reactの保守性はコード量ではなく、責務の分け方と状態設計で決まります。

Reactアンチパターン10選の一覧

まず、この記事で扱うReactアンチパターンを一覧で整理します。

番号アンチパターン起こる問題
11コンポーネントに全部書く修正箇所が分かりにくい
2Props Drillingを放置する値の流れが追いにくい
3計算できる値をstateにする管理対象が増える
4useEffectで状態同期する再レンダリングが増える
5親コンポーネントにstateを集めすぎる変更の影響範囲が広がる
6コンポーネントの責務が曖昧保守性が下がる
7keyにindexを使う表示バグが起きやすい
8React.memoを乱用するコードが複雑になる
9useCallbackを目的なく使う可読性が下がる
10API取得・変換・表示を混ぜるテストや修正が難しくなる

ここからは、それぞれのReactアンチパターンについて、悪い例、なぜ問題なのか、改善の考え方を順番に見ていきます。

Reactアンチパターン1:1コンポーネントに全部書く

まず、React初心者が最もやりがちなアンチパターンは、1つのコンポーネントにすべての処理を書くことです。

例えば、最初は小さい画面でも、機能追加を続けるうちに、state管理、API取得、バリデーション、一覧表示、モーダル制御が1ファイルに集まっていきます。

悪い例:巨大なUserPage

function UserPage() {
  // state管理
  // API取得
  // バリデーション
  // テーブル表示
  // モーダル管理
  // ボタン処理

  // 500行続く...
}

改善例:意味のある単位で分割する

function UserPage() {
  return (
    <>
      <UserProfile />
      <UserPostList />
      <UserModal />
    </>
  );
}

一方で、コンポーネントを意味のある単位で分けると、どこを修正すればよいかが明確になります。ただし、細かく分けすぎると逆に追いにくくなるため、「表示の意味」「変更理由」「責務」を基準に分割します。

Reactアンチパターン2:Props Drillingを放置する

次に、Props Drillingとは、親から子へ、さらにその子へ、さらにその子へとpropsを渡し続けることです。

Props Drillingのイメージ

App
↓
Layout
↓
Page
↓
UserSection
↓
UserCard

例えば、途中のLayoutやPageがその値を使っていない場合、propsを渡すためだけのコードが増えます。その結果、値の流れが追いにくくなります。

改善するときは、まず本当に深く渡す必要があるかを確認します。ログインユーザー、テーマ設定、言語設定のように広い範囲で使う値はContextや状態管理ライブラリを検討します。一方で、一部画面だけの入力値までグローバル化する必要はありません。

なお、React公式のSharing State Between Componentsでも、stateごとに「どのコンポーネントが所有するか」を考えることが説明されています。

Reactアンチパターン3:計算できる値をstateにする

また、計算すれば分かる値までstateにするのも、よくあるReactアンチパターンです。

なぜなら、stateが増えるほど、更新タイミング、初期値、同期漏れを考える必要があるからです。そのため、propsや他のstateから計算できる値は、基本的にその場で計算します。

悪い例:fullNameをstateにする

const [fullName, setFullName] = useState("");

useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

改善例:レンダリング中に計算する

const fullName = `${firstName} ${lastName}`;
避けたい例改善例
フルネームをstateにする文字列結合で作る
件数をstateにするarray.lengthで計算する
合計金額をstateにするreduceで計算する
絞り込み結果をstateにするfilterで計算する

React公式のYou Might Not Need an Effectでも、表示のために変換できるデータはEffectではなくレンダリング中に計算する考え方が紹介されています。

Reactアンチパターン4:useEffectで状態同期する

一方で、useEffectは便利ですが、何でも書く場所ではありません。特に、state同士を同期するためだけのuseEffectは要注意です。

悪い例:items.lengthをuseEffectで同期する

const [count, setCount] = useState(0);

useEffect(() => {
  setCount(items.length);
}, [items]);

改善例:必要なときに計算する

const count = items.length;

状態同期useEffectの問題

items変更
↓
再レンダリング
↓
useEffect実行
↓
setCount
↓
再レンダリング

つまり、useEffectはReactの外側にあるものと同期するための仕組みです。例えば、ブラウザAPI、イベント登録、タイマー、外部システムとの接続などです。単なる計算や表示用データの変換なら、Effectを使わない方が読みやすくなります。

Reactアンチパターン5:親コンポーネントにstateを集めすぎる

次に、親コンポーネントにstateを集めすぎると、画面全体の責務が大きくなります。

もちろん、複数の子コンポーネントで共有するstateは親に置く必要があります。しかし、検索フォームだけで使う値、モーダルだけで使う開閉状態まで親に集めると、親コンポーネントが読みにくくなります。

State置き場所の考え方
検索キーワードSearchBoxまたは検索条件を扱う範囲
モーダル開閉Modalを管理するコンポーネント周辺
ページ番号Paginationや一覧の制御範囲
ログインユーザーアプリ全体で使うならContextなどを検討

基本は、stateを使う場所に近づけます。ただし、複数のコンポーネントで同じ値を同期したい場合は、共通の親へ持ち上げます。このバランスがReactの状態設計では重要です。

Reactアンチパターン6:コンポーネントの責務が曖昧

さらに、コンポーネントの名前と中身が一致していないコードも危険です。

悪い例:UserCardが表示以外も担当する

function UserCard() {
  // API取得
  // バリデーション
  // モーダル管理
  // 表示
}

改善例:取得と表示を分ける

function UserPage() {
  const users = useUsers();

  return <UserList users={users} />;
}

そのため、コンポーネントは「何を表示するか」、Custom Hookは「どう取得・管理するか」、純粋関数は「どう変換するか」を担当させると、コードの読み手が役割を理解しやすくなります。

Reactアンチパターン7:keyにindexを使う

また、リスト表示でkeyにindexを使うのも、Reactでよく見るアンチパターンです。

悪い例:indexをkeyにする

users.map((user, index) => (
  <UserCard key={index} user={user} />
));

改善例:一意なIDをkeyにする

users.map((user) => (
  <UserCard key={user.id} user={user} />
));

React公式のRendering Listsでは、keyは配列内の要素を識別するために必要だと説明されています。並び替え、追加、削除があるリストでは、indexではなくデータが持つ一意なIDを使うのが基本です。

Reactアンチパターン8:React.memoを乱用する

次に、React.memoはpropsが変わっていないときに再レンダリングをスキップするための機能です。ただし、すべてのコンポーネントに付ければ速くなるわけではありません。

悪い例:何も考えずにmemo化する

export default React.memo(UserCard);

そのため、React.memoを乱用すると、「なぜmemo化しているのか」がわからないコードになります。また、propsに毎回新しいオブジェクトや関数を渡している場合、期待した効果が出ないこともあります。

React公式のmemoでも、propsが変わらないときに再レンダリングをスキップする仕組みとして説明されています。まずは状態の置き場所やコンポーネント分割を見直し、それでも描画が重い場合に使うのが現実的です。

Reactアンチパターン9:useCallbackを目的なく使う

同じように、useCallbackも誤解されやすいHookです。useCallbackは関数そのものを速くするHookではありません。関数の参照を安定させるためのHookです。

悪い例:すべての関数をuseCallbackで包む

const handleClick = useCallback(() => {
  console.log("click");
}, []);

例えば、この関数をmemo化した子コンポーネントに渡していないなら、useCallbackの効果は限定的です。むしろ依存配列を考える手間が増え、可読性が下がることがあります。

useCallbackが効きやすい場面

親が再レンダリング
↓
関数が再生成される
↓
memo化した子に新しいpropsとして渡る
↓
子が再レンダリングされる
↓
useCallbackで参照を安定させる

つまり、useCallbackはReact.memoとセットで意味を持つことが多いです。公式のuseCallbackも、関数定義をキャッシュするHookとして説明されています。

Reactアンチパターン10:API取得・変換・表示を混ぜる

最後に、実務で特に保守性を下げやすいReactアンチパターンが、API取得、データ変換、表示を1つのコンポーネントに混ぜることです。

悪い例:取得、変換、表示が混ざる

function UserPage() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    fetchUsers()
      .then(users => users.map(user => ({
        id: user.id,
        name: `${user.firstName} ${user.lastName}`,
      })))
      .then(setUsers);
  }, []);

  return (
    <div>
      {users.map(user => (
        <p key={user.id}>{user.name}</p>
      ))}
    </div>
  );
}

改善例:取得、変換、表示を分ける

function UserPage() {
  const users = useUsers();

  return <UserList users={users} />;
}

データ取得はCustom Hook、データ変換は関数、表示はComponentに分けると、変更理由が分かれます。その結果、テストしやすく、レビューしやすく、引き継ぎやすいコードになります。

Reactアンチパターンを見つける危険サイン

なお、次のような状態になっていたら、React設計を見直すタイミングです。

危険サイン起きている可能性要注意度
useEffectが5個以上ある状態設計が崩れている
stateが20個以上ある責務が大きすぎる
コンポーネントが500行を超える分割不足
propsが10個以上あるProps Drillingの可能性
React.memoだらけ根本原因を隠している
eslint-disableが多い依存関係をごまかしている

もちろん、これらの数字だけで悪い設計だと決めつける必要はありません。ただし、レビューや改修で何度も迷うなら、コンポーネント分割、stateの置き場所、useEffectの必要性を見直す価値があります。

良いReact設計に近づけるチェックリスト

  • このコンポーネントは1つの責務に集中しているか?
  • 計算できる値をstateにしていないか?
  • useEffectで不要な状態同期をしていないか?
  • propsを深く渡しすぎていないか?
  • React.memoやuseCallbackを目的なく使っていないか?
  • API取得、データ変換、表示が混ざっていないか?

つまり、良いReact設計とは、特別なテクニックを詰め込むことではありません。どこに何を書くかが明確で、変更理由ごとにコードが分かれている状態です。

まとめ:Reactアンチパターンを避けて変更しやすいコードを書く

今回は、保守性が下がるReactアンチパターンを10個紹介しました。

避けたいこと意識したいこと
1コンポーネントに全部書く意味のある単位で分割する
計算値をstateにする必要な時に計算する
useEffectを乱用する本当に副作用か考える
React.memoを過信するまず設計を見直す
API取得と表示を混ぜる責務を分離する

Reactは「動かす」よりも「変更しやすく作る」方が難しいです。実務では、新規開発よりも保守・改修の時間が長くなることも多いため、保守性を意識した設計が重要になります。

React・Next.jsの実務スキルを伸ばしたい方は、まず「どこに何を書くか」を整理するところから始めてみてください。

IaC INP PM PMO PMP UX Webディレクター インフラエンジニア キャリアチェンジ フロントエンドエンジニア