【テクニカル・上級編】 Propsの読み取り専用制約とreadonly修飾子 – React実践ガイド

Reactの「Props不変性」を型レベルで強制する:堅牢なコンポーネント設計の美学

Reactにおいて「Propsは読み取り専用である」というのは、公式ドキュメントの最初の方に書かれている基本原則だ。しかし、中規模以上のプロジェクトで、数人のエンジニアが入れ替わり立ち替わりコードを触っていると、誰かが無意識のうちに「Propsのプロパティを直接書き換える」という地雷を踏むことがある。

「え、`const props`なんだから書き換えられるわけがないだろう?」と思うかもしれない。だが、TypeScriptの型定義が甘いと、参照型(オブジェクトや配列)の内部プロパティは平気で書き換えられてしまう。これがレンダリングの不整合や、予測不能なバグの温床になることを、我々のようなフロントエンドの現場主義者は痛いほど知っているはずだ。

今日は、TypeScriptの `readonly` 修飾子を駆使し、コンパイル時点でPropsの「不変性」を担保する、防御的アーキテクチャの話をしよう。

—

なぜ `readonly` を明示的に課すべきなのか

Reactのレンダリングサイクルにおいて、Propsの変更は「親から子へ流れる単一方向」であるべきだ。もし子が受け取ったPropsを直接書き換えてしまうと、以下のような致命的な問題が連鎖する。

1. メモリ効率と参照透過性の欠如:
Reactは、Propsの比較(`memo` や `useMemo` の依存配列)において参照の等価性(Referential Equality)をチェックする。もしPropsが意図せず書き換えられると、メモ化が機能不全に陥り、再レンダリングの嵐が吹き荒れる。
2. 非同期処理の競合(レースコンディション):
非同期関数の内部でPropsの値を参照しているとき、レンダリング間でその値が変わってしまうと、本来意図しない古い状態や新しい状態が混在し、UIとロジックの不整合が発生する。
3. バグの温床:
「なぜか親コンポーネントの状態まで変わってしまった」という事態は、JavaScriptのオブジェクト参照の仕様を知らないジュニアエンジニアが遭遇する最も悲惨なバグの一つだ。

—

実践:`readonly` で型をガードする

では、具体的にどう実装するか。単純に `interface` を書くのではなく、プロパティ一つひとつに `readonly` を付与する癖をつける。

/

  • 堅牢なProps定義のサンプル
  • 外部からの不意な書き換えをコンパイルエラーで阻止する

/
interface UserProfileProps {
readonly id: string;
readonly name: string;
// オブジェクトのネスト構造も再帰的にreadonlyを適用するのがプロの流儀
readonly preferences: {
readonly theme: ‘light’ | ‘dark’;
readonly notifications: boolean;
};
readonly tags: readonly string[]; // 配列もreadonlyにするのを忘れてはならない
}

const UserProfile: React.FC = ({ name, preferences }) => {
// 以下のコードはコンパイルエラーになる
// preferences.theme = ‘light’;
// ↑ これにより、意図しない副作用を即座に検知できる

return

{name} – {preferences.theme}

;
};

もし、型定義が膨大で個別に `readonly` をつけるのが面倒なら、TypeScriptの組み込みユーティリティ型 `Readonly` を使うといい。ただし、階層が深い場合は `DeepReadonly` を自作するか、ユーティリティライブラリ(`ts-essentials` など)を活用するのが賢い選択だ。

—

パフォーマンスへの隠れた恩恵

`readonly` を徹底することは、単なるバグ防止ではない。実は、Reactのレンダリング最適化を「強制する」ための土台でもある。

コンポーネントが「Propsは絶対に変わらない(親から新しいオブジェクトが渡されない限り)」という前提を持てるようになると、`React.memo` の設計が非常にシンプルになる。Propsが不変であることが保証されていれば、浅い比較(Shallow Comparison)だけで十分にレンダリングの抑止が可能になるからだ。

逆に、Propsが「いつ書き換えられるかわからない」状態であれば、常に深い比較(Deep Comparison)を強いられ、それは計算量的に非常に高コストな処理になる。

アーキテクトとしてのアドバイス

私が現場でよく行うのは、「Propsをそのまま加工して使わない」という設計ルールだ。

Propsを直接いじりたくなった時、それは「そのコンポーネントの責務が大きすぎる」というサインである。そんな時は、素直に `useState` や `useMemo` で新しい状態として切り出すか、親コンポーネント側でデータを加工してから渡すように設計を見直すべきだ。

  • Props: データの読み取り専用の入り口。
  • State: コンポーネント内部で管理する「変化するデータ」。

この境界線を `readonly` で物理的に分かつことは、コードの可読性を高めるだけでなく、チームメンバーへの無言の「制約という名の優しさ」になる。

まとめ:技術の規律が、プロダクトの寿命を延ばす

「動けばいい」というコードは、書いた本人以外の全員を不幸にする。`readonly` 修飾子という小さなガードレールは、大規模なアプリケーションにおいて「何が変更可能で、何が変更不可能か」という明確な契約をコードに刻み込む。

TypeScriptの型システムは、単なるバリデーターではない。それは、あなたが設計したアーキテクチャの意思を、後からコードを読む人々に伝えるための「ドキュメント」そのものだ。

今日からあなたのコンポーネントのPropsに、`readonly` の灯火を灯してほしい。それだけで、コードの品質は一段階上のステージへと引き上げられるはずだ。

コメント

タイトルとURLをコピーしました