定義
エディタ・フォームビルダーのように頻繁に変わる大きな状態を複数のコンポーネントが一緒に見なければならないとき、Contextを誤って使うと無関係な部分まで全部再描画される問題を扱う。React Context render granularity(コンテキスト再レンダーの粒度)は、Context Provider(下位ツリーに値を下ろすコンポーネント)のvalueに頻繁に変更される大きな状態(文書ツリー全体、リアルタイムの全一覧など)を入れるとき、そのContextを購読するすべての下位コンポーネントが変更のたびに再レンダーされる状況を指す。このとき、React.memo(propsが変わらなければ再レンダーを止めてくれるラッパー)・propsの同一性・structural sharing(構造的共有: 変わった部分だけ新しく作り、残りは再利用)といった最適化が全部無力化される。解法は購読範囲を狭めるProvider分離 — 頻繁に変わる断片と変わらない断片を別々のProviderに分けることである。
文脈で理解する
行単位の再レンダーをO(1)に減らしたあとでも、最上位のProviderがuseStateで文書全体を持っていれば、入力1回ごとにProvider関数が再実行される。子の行はexternal storeで隔離したのに、外殻コンテナだけが毎回回る「最後の尻尾」が残る。このパターンは頻繁に変わる断片をContext valueから外し、固定されたstore + useSyncExternalStoreで各UI断片が必要なスナップショットだけを購読するようにする。
なぜ必要か
エディタ・フォームビルダーは、actions・meta・live documentを1つのEditorProviderにまとめやすい。本文が1文字変わるたびにvalueオブジェクトが新しい参照になると、目次・統計・ツールバーまで関係のないブロックが全部再レンダーされる。大規模ツリーでは入力遅延・テスト失敗(render count)につながる。
動作原理
- Consumerは
useContextでProvider valueを購読する。 - Providerがre-renderされると、value参照が変わる限りすべてのconsumerがre-renderされる — 中間のmemoがpropsを止めても、context購読は止められない。
value={{ handlers, document }}のinlineオブジェクトは毎renderで新しい参照になる。- documentだけが変わっても、handlersだけを使う子まで更新される。
- 狭いContextはdocumentと派生統計だけを持ち、TOC・word countコンポーネントだけをそのProviderの下に置く。
- 広いruntime contextは
useCallback/useMemoで安定化されたactions・metaだけを保つ。
実務適用
- ライブ派生(heading一覧、文字数、sync状態)は専用のContextまたは外部store + selectorに。
- runtime bagにはdispatch・stableなcallback ref・readOnlyフラグなど、変わらないか安定化されたものだけを。
- ブロック/行単位の
renderCountテストで「入力1回でN個のブロックだけ更新」の回帰を捕まえる。 - closed union(インラインノードのvariant)の拡張時に
type === 'text'を仮定する箇所をguard — 型チェックが波及を明らかにする。
トレードオフ
- Provider分離はボイラープレート・ツリーの深さを増やす。
- context selectorライブラリ・React Compilerは一部のケースを緩和するが、設計段階での分離が依然として明確である。
- 過度なmemoはデバッグを難しくする — render countテストで意図を文書化する。
使ってはいけない場合
- 小さいフォーム(フィールド3個) — context分離のコストが利得より大きい。
- すでにグローバルstore(Zustand selectorなど)でgranularityが解決された場合 — Contextが重複。
- サーバーコンポーネントだけのサブツリー — client contextの問題は該当しない。
よくある間違い
useMemo(() => ({ ...handlers, doc }), [handlers, doc])でdocをruntimeに含め続ける。- 派生統計を毎renderでinline計算してvalueに入れる。
- memoされたBlockが、context購読を子ではなく自分でさせる。
- union拡張後にmarkdown・range演算でtext-onlyの仮定を維持する。
関連概念
- focused-guest-editor — host/guest・購読境界
- usesyncexternalstore-snapshot-identity — 外部storeのスナップショット
- list-virtualization-windowing — 大量リストのレンダー削減