Reactのstateが残るか消えるかは位置と種類で決まる
React で表示を切り替えると、前の入力やカウントが残ることも、消えることもあります。どちらになるかは、画面のツリーの同じ位置に、同じ種類のコンポーネントが描画され続けているかで決まります。種類とは、Counter のような関数、または div や p のような要素の名前のことです。
タブや「編集/表示」のモードを条件付きレンダーで出し分けたときに、入力した内容が消えた、または前の画面の値が残った、という場合の原因を探す手がかりになります。私は検証環境で、クリックした回数を state に持つカウンターを5通りの書き方で出し分け、切り替えたあとに回数が残るかを確かめました。
検証環境は Ubuntu 24.04、Node.js 22.23.4、react / react-dom 19.3.1、TypeScript 7.0.2 です。.tsx を tsc で型チェックしてから esbuild で開発用にまとめ、Playwright 1.63.0 で動かした Chromium 153 で開いてクリックしました。マウントのやり直しが記録に混ざらないよう、StrictMode は使っていません。Vite や Next.js の上での動作、Chromium 以外のブラウザーでの動作は試していません。
検証に使ったカウンター
Counter は、押した回数を count という state に持つボタンです。エフェクトで、マウントとアンマウントを記録します。log は、記録を残すだけの検証用の関数です。
// Counter.tsx
import { useEffect, useState } from 'react';
import { log } from './log';
// クリックした回数を state に持つカウンター
export function Counter({ label }: { label: string }) {
const [count, setCount] = useState(0);
useEffect(() => {
log(`マウント(${label})`);
return () => log(`アンマウント(${label})`);
}, []);
return (
<button type="button" className="counter" onClick={() => setCount(count + 1)}>
{label}:{count}回
</button>
);
}
どの書き方でも、カウンターを3回押してから「切り替え」を押し、もう一度「切り替え」を押して元の表示に戻しました。切り替えの前に、カウンターのボタンの要素に data-mark 属性で目印を書き込んでおき、切り替えたあとも同じ DOM の要素が使われているかを調べています。
三項演算子で同じ Counter を出し分けると、回数が残る
1つ目は、三項演算子で label だけが違う Counter を出し分ける書き方です。
export function SameType() {
const [isFancy, setIsFancy] = useState(false);
return (
<div>
{isFancy ? <Counter label="派手なカウンター" /> : <Counter label="普通のカウンター" />}
<button type="button" className="toggle" onClick={() => setIsFancy(!isFancy)}>切り替え</button>
</div>
);
}
[マウント(普通のカウンター)]
> カウンターを3回クリック
画面: 普通のカウンター:3回
> 「切り替え」をクリック(1回目)
画面: 派手なカウンター:3回
ボタンの DOM は前と同じ要素か: true
> 「切り替え」をクリック(2回目)
画面: 普通のカウンター:3回
表示は「派手なカウンター」に変わりましたが、回数は3回のままでした。アンマウントもマウントも記録されておらず、ボタンの要素も前と同じです。JSX には <Counter> を2回書いていますが、React から見ると、div の1番目の子に Counter が描画され続けているだけで、label という props が変わったにすぎません。
公式ドキュメントの state の保持とリセットのページでも、同じ位置に同じコンポーネントがあれば state は保たれると説明されています。大事なのは JSX の書き方ではなく、描画した結果のツリーの中での位置です。同じ節の注意の囲みには、if の中と外で別々に return した <Counter /> も、同じ位置なので state が残る例があります。
同じ位置で種類が変わると、回数が消える
2つ目は、同じ位置に Counter と <p> を出し分ける書き方です。
export function DifferentType() {
const [isPaused, setIsPaused] = useState(false);
return (
<div>
{isPaused ? <p>休憩中</p> : <Counter label="カウンター" />}
<button type="button" className="toggle" onClick={() => setIsPaused(!isPaused)}>切り替え</button>
</div>
);
}
[マウント(カウンター)]
> カウンターを3回クリック
画面: カウンター:3回
> 「切り替え」をクリック(1回目)
[アンマウント(カウンター)]
画面: <p>休憩中</p>
> 「切り替え」をクリック(2回目)
[マウント(カウンター)]
画面: カウンター:0回
<p> に切り替えた時点で Counter はアンマウントされ、戻したときは新しくマウントされて、0回から始まりました。同じ位置でも、種類が変わると React はそれまでのコンポーネントを state ごと取り除きます。
下の GIF は、1つ目と2つ目の書き方を上下に並べ、1つの「切り替え」ボタンで両方を同時に切り替えたデモです。

上は表示が A から B に変わっても2回のままです。下は、いったん「休憩中(p 要素)」になり、戻すと0回になりました。
包む要素が変わると、中の Counter も作り直される
3つ目は、Counter 自体は同じで、それを包む要素だけが div と section で変わる書き方です。
export function DifferentWrapper() {
const [isFancy, setIsFancy] = useState(false);
return (
<div>
{isFancy ? (
<section><Counter label="sectionの中" /></section>
) : (
<div><Counter label="divの中" /></div>
)}
<button type="button" className="toggle" onClick={() => setIsFancy(!isFancy)}>切り替え</button>
</div>
);
}
[マウント(divの中)]
> カウンターを3回クリック
画面: divの中:3回
> 「切り替え」をクリック(1回目)
[アンマウント(divの中)]
[マウント(sectionの中)]
画面: sectionの中:0回
ボタンの DOM は前と同じ要素か: false
> 「切り替え」をクリック(2回目)
[アンマウント(sectionの中)]
[マウント(divの中)]
画面: divの中:0回
Counter は同じ関数で、包む要素の中の位置も同じなのに、回数は0回に戻り、ボタンの要素も新しいものに替わりました。Counter を包む div が section に変わったことで、React はその下にあるものをまとめて作り直しています。
同じページの、別のコンポーネントを同じ位置に描画した場合の説明にあるとおり、種類が変わると、その下のツリー全体の state がリセットされます。state を残したいなら、描画のたびにツリーの形がそろっている必要がある、とも書かれています。
&& を2つ並べると、別の位置になる
4つ目は、三項演算子の代わりに && を2つ並べる書き方です。出し分けているのは1つ目と同じ Counter です。
export function TwoPositions() {
const [isFancy, setIsFancy] = useState(false);
return (
<div>
{isFancy && <Counter label="派手なカウンター" />}
{!isFancy && <Counter label="普通のカウンター" />}
<button type="button" className="toggle" onClick={() => setIsFancy(!isFancy)}>切り替え</button>
</div>
);
}
[マウント(普通のカウンター)]
> カウンターを3回クリック
画面: 普通のカウンター:3回
> 「切り替え」をクリック(1回目)
[アンマウント(普通のカウンター)]
[マウント(派手なカウンター)]
画面: 派手なカウンター:0回
ボタンの DOM は前と同じ要素か: false
> 「切り替え」をクリック(2回目)
[アンマウント(派手なカウンター)]
[マウント(普通のカウンター)]
画面: 普通のカウンター:0回
こちらは回数が0回に戻りました。{isFancy && ...} と {!isFancy && ...} は div の1番目と2番目の子で、公式ドキュメントにあるとおり、どちらかが false のときもその位置は空いたまま残ります。そのため、片方の Counter が消え、もう片方の位置に別の Counter が現れた、という扱いになります。
同じ Counter でも、三項演算子なら回数が残り、&& を2つ並べると消えます。同じページの、違う位置に描画して state を分ける方法では、この性質を使って、2人のプレイヤーのスコアを別々にリセットする例が紹介されています。消すつもりがなければ三項演算子でまとめ、意図して分けたいなら && で位置を分けるか、key を変えます(key で state をリセットする方法)。
中身が同じでも、別の関数なら別の種類
5つ目は、Counter と中身がまったく同じ CounterCopy という関数を作り、三項演算子で出し分ける書き方です。
{useCopy ? <CounterCopy label="CounterCopy" /> : <Counter label="Counter" />}
[マウント(Counter)]
> カウンターを3回クリック
画面: Counter:3回
> 「切り替え」をクリック(1回目)
[アンマウント(Counter)]
[マウント(CounterCopy)]
画面: CounterCopy:0回
ボタンの DOM は前と同じ要素か: false
> 「切り替え」をクリック(2回目)
[アンマウント(CounterCopy)]
[マウント(Counter)]
画面: Counter:0回
描画される HTML の形は同じでも、回数は0回に戻りました。React が種類として比べているのは関数そのもので、コードの中身ではありません。
コンポーネントの関数の中で、別のコンポーネントの関数を定義すると、親が描画されるたびに新しい関数ができます。React から見ると毎回別の種類になるので、その下の state が描画のたびに消えます。公式ドキュメントの注意では、コンポーネントの関数はファイルの一番外側で定義し、入れ子にしないよう勧めています。
同じ位置に同じ種類(同じ関数、または同じ名前の要素)が描画され続けるかぎり、props が変わっても state は残ります。位置か種類のどちらかが変わると、その下の state はまとめて消えます。





