Skip to main content

Provider の key remount で state をリセットするのは anti-pattern — in-flight 通知が消える

TL;DR​

  • <Provider key={projectId}> のように key を変えて Provider を unmount/remount すると、state はリセットされるが配下の全 children の state も巻き込んで破棄される。
  • Toast・LiveRegion など in-flight(処理中)の通知がドロップされる事故につながる。
  • key remount は撤去し、Provider は App 直下に hoist。切り替え時は「同じ画面を新しいデータで再描画するだけ」で state 強制リセットは不要。
  • 消費者が 1 つしかない Context は、そもそも useState で足りることが多い。

1. 問題​

project 切り替え時に、key で Provider を作り直して state をリセットする手法を採っていた。

// project が変わるたびに Provider ごと作り直す
<ProjectProvider key={projectId}>
<AppShell />
</ProjectProvider>

key が変わると React は古い ProjectProvider を unmount し、新しいものを mount し直す。狙いは「前 project の state を確実に捨てる」ことだが、副作用として AppShell 配下のすべての state も一緒に破棄される。

その結果、AppShell の中で表示中だった Toast や LiveRegion の in-flight 通知が remount で消える。ユーザーがまだ読んでいない通知が、project 切り替えの瞬間に無言でドロップされる。

2. 原因​

key の変更は React にとって「別のコンポーネント」を意味する。ツリー全体を作り直すので、リセットしたかった state だけでなく、その Provider の子孫が持つ無関係な state まで巻き添えになる。

思考モデルの誤りとして、React Router の挙動と混同しているケースが多い。route が変わっても state を強制リセットする必要はなく、同じ画面なら新しいデータで再描画するだけでよい。key remount は「作り直し」という強い手段を、単なる「再描画」で済む場面に持ち込んでいる。

3. 解決​

  • key を撤去する。
  • Provider は App 直下に hoist し、project 切り替えでは state を新しい値で更新するだけにする。
  • project 変更に伴うリセットが必要なら、useEffect(依存に projectId)や derived state で必要な部分だけを更新する。ツリーごと作り直さない。
// App 直下に 1 度だけ mount。切り替えは state 更新で表現する
<ProjectProvider>
<AppShell />
</ProjectProvider>

さらに、この Context の消費者が 1 箇所しかないなら、Context 自体が過剰なことも多い。その場合は useState に落とすと Provider 階層自体が消える。

4. 判断のポイント​

  • key remount は「ツリーごと捨てる」核弾頭。リセットしたいのが特定の state だけなら、そこだけを更新する手段(state 更新・useEffect・derived state)を選ぶ。
  • key remount を検討したら、まず「配下に in-flight な state(通知・アニメーション・入力途中の値など)がないか」を確認する。あるなら巻き添えで消える。
  • 「route/選択対象が変わったら state を全部リセットしたい」は多くの場合思い込み。同じ画面を新データで描画するだけで足りないか先に疑う。
  • 消費者が 1 つの Context は useState で十分。Provider を残す理由が薄いなら畳む。