keyを変えて、Reactコンポーネントの状態をリセットする方法
チャットの相手を切り替えたのに、前の相手あての書きかけの文が入力欄に残ることがあります。React は、同じ場所に描画された同じコンポーネントの state を引き継ぐからです。コンポーネントに key を渡し、相手が変わったら key も変わるようにすると、React はそのコンポーネントを作り直し、state を初めに戻します。
筆者は検証環境で、key がない場合とある場合の動きを jsdom と Chromium で比べました。あわせて、エフェクトで state を空にする書き方との違い、一部の state だけを戻す方法、key に使ってはいけない値も確かめています。
検証環境は Ubuntu 24.04、Node.js 22.22.0 です。使ったのは react / react-dom 19.3.1、@types/react 19.3.0、typescript 7.0.2、esbuild 0.28.2、jsdom 29.1.1 です。.tsx を tsc で型チェックしてから esbuild で変換し、jsdom の上で動かしました。StrictMode は使っていません。GIF は、Chromium 141 を Playwright 1.56.1 で操作して撮りました。
同じ場所の同じコンポーネントは、state を引き継ぐ
検証には、送る相手ごとにメッセージを書く Chat を使いました。書きかけの文(draft)を state に持ち、子の SendOptions も「急ぎ」のチェックを state に持っています。log は、描画とエフェクトの様子を記録する検証用の関数です。
// Chat.tsx
import { useEffect, useState } from 'react';
import type { Contact } from './contacts';
import { log } from './log';
export function Chat({ contact }: { contact: Contact }) {
const [draft, setDraft] = useState('');
log(`描画 Chat(${contact.name}) draft="${draft}"`);
useEffect(() => {
log(`エフェクト開始(${contact.name})`);
return () => {
log(`後片付け(${contact.name})`);
};
}, [contact.id]);
return (
<section className="chat">
<textarea
value={draft}
placeholder={`${contact.name} へのメッセージ`}
onChange={(e) => setDraft(e.target.value)}
/>
<SendOptions />
<button>{contact.name} に送る</button>
</section>
);
}
// 子のコンポーネントにも state を持たせる
function SendOptions() {
const [urgent, setUrgent] = useState(false);
return (
<label>
<input
type="checkbox"
checked={urgent}
onChange={(e) => setUrgent(e.target.checked)}
/>
急ぎ
</label>
);
}
使う側では、相手の一覧(ContactList)で選んだ相手を Chat に渡します。まずは key を付けません。
// Messenger.tsx(抜粋。import と ContactList は省略)
// key なし:相手を変えても、同じ Chat が使い回される
export function MessengerNoKey() {
const [toId, setToId] = useState('alice');
const to = contacts.find((c) => c.id === toId)!;
return (
<div>
<ContactList onSelect={setToId} />
<Chat contact={to} />
</div>
);
}
Alice あてに「こんにちは」と書き、「急ぎ」にチェックを入れてから、Bob に切り替えました。
--- 1a. <Chat contact={to} />(key なし) ---
(中略)
> Bob をクリック
[描画 Chat(Bob) draft="こんにちは"]
[後片付け(Alice)]
[エフェクト開始(Bob)]
画面: textarea="こんにちは" placeholder="Bob へのメッセージ" 急ぎ=true
textarea の DOM は前と同じ要素か: true
プレースホルダーは「Bob へのメッセージ」に変わりましたが、Alice あての「こんにちは」と「急ぎ」のチェックは残りました。textarea の DOM も、前と同じ要素のままです。このまま送ると、Alice に書いた文を Bob に送ってしまいます。
state の保持とリセットのページにある、state と描画ツリーの位置の説明によると、state はコンポーネントの中ではなく React が持っていて、描画ツリーのどの位置にあるかで、どのコンポーネントのものかを決めています。同じ位置に同じコンポーネントを描画し続けるかぎり、props が変わっても state は残ります(同じ位置の同じコンポーネントについての説明)。
なお、エフェクトは依存配列に contact.id があるので、key がなくても、相手が変わるたびに後片付けと開始をやり直しています。
key を渡すと、別のコンポーネントとして作り直される
変えるのは1か所だけです。Chat に、相手の ID を key として渡します。
// Messenger.tsx(抜粋)
// key あり:相手が変わると、Chat を作り直す
export function MessengerWithKey() {
const [toId, setToId] = useState('alice');
const to = contacts.find((c) => c.id === toId)!;
return (
<div>
<ContactList onSelect={setToId} />
<Chat key={to.id} contact={to} />
</div>
);
}
同じ操作をした結果です。
--- 1b. <Chat key={to.id} contact={to} /> ---
(中略)
> Bob をクリック
[描画 Chat(Bob) draft=""]
[後片付け(Alice)]
[エフェクト開始(Bob)]
画面: textarea="" placeholder="Bob へのメッセージ" 急ぎ=false
textarea の DOM は前と同じ要素か: false
> Alice をクリック
[描画 Chat(Alice) draft=""]
[後片付け(Bob)]
[エフェクト開始(Alice)]
画面: textarea="" placeholder="Alice へのメッセージ" 急ぎ=false
Bob に切り替えると、入力欄が空になり、子の SendOptions の「急ぎ」のチェックも外れました。textarea は新しい DOM の要素に替わっています。Alice に戻しても「こんにちは」は戻らず、空のままでした。作り直したときに、前の state は捨てられています。
Chromium で key なしと key ありの Chat を上下に並べ、入力欄に書いてから相手を切り替える様子を撮ったのが次の GIF です(GIF では「急ぎ」のチェックは入れていません)。上の「送る相手」のボタンを押すと、上下どちらの Chat も送る相手が切り替わります。

同じページの、key で state をリセットする方法の説明によると、key を指定すると、React は親の中での順番ではなく key そのものを位置の一部として使います。そのため、JSX の同じ場所に書いた Chat でも、key が違えば別のコンポーネントとして扱い、state を共有しません。
key は、親の中の兄弟どうしを区別するものです。アプリ全体で重ならない値にする必要はありません(同じ説明のあとにある注記)。また、key は props としてコンポーネントに渡りません。Chat が相手の情報を使うなら、検証のコードのように contact を別に渡します(リストの描画のページにある、key についての注意)。
エフェクトで空にする書き方との違い
key を使わずに、ID が変わったらエフェクトで state を空にする書き方もできます。エフェクトが要らない場面のページにある、props が変わったときに state をすべて戻す例を元に、両方を書いて比べました。CommentBox は、受け取った comment を入力欄に出し、描画するたびに記録します。
// EffectReset.tsx(抜粋。import と CommentBox は省略)
// 🔴 userId が変わったら、エフェクトで comment を空にする
export function ProfileEffect({ userId }: { userId: string }) {
const [comment, setComment] = useState('');
useEffect(() => {
setComment('');
}, [userId]);
return <CommentBox userId={userId} comment={comment} onChange={setComment} />;
}
// ✅ 外側で key を渡す。使う側は userId を普通に渡すだけでよい
export function ProfileKey({ userId }: { userId: string }) {
return <Profile key={userId} userId={userId} />;
}
function Profile({ userId }: { userId: string }) {
const [comment, setComment] = useState('');
return <CommentBox userId={userId} comment={comment} onChange={setComment} />;
}
「すてきな写真」と入力してから、userId を alice から bob に変えました。
--- 2a. エフェクトで空にする(ProfileEffect) ---
(中略)
> userId を alice から bob に変える
[描画 CommentBox userId=bob comment="すてきな写真"]
[描画 CommentBox userId=bob comment=""]
input の値: ""
--- 2b. key で作り直す(ProfileKey) ---
(中略)
> userId を alice から bob に変える
[描画 CommentBox userId=bob comment=""]
input の値: ""
どちらも最後は空になりました。ただ、エフェクトの書き方では、bob のプロフィールに、alice のプロフィールで入力したコメントが入ったまま、いったん描画されています。そのあとエフェクトが動き、もう一度描画して空になりました。key の書き方では、最初の描画から空です。
リンク先のページでも、エフェクトの書き方は、古い値で描画してからもう一度描画するので効率が悪いとされています。中に state を持つコンポーネントが増えるたびに、空にする処理を書き足す必要もあります。key なら、作り直すコンポーネントの下にある state がまとめて初めに戻ります。
ProfileKey のように外側のコンポーネントで key を付けておくと、使う側は <ProfileKey userId={id} /> と書くだけで済みます。key で作り直していることは、使う側からは見えない内部の作りとして隠しておけます(同じ例のあとの説明)。
一部の state だけを戻したいとき
key で作り直すと、そのコンポーネントの state はすべて初めに戻ります。コメントは空にしたいが、星の数(rating)は残したい、という場合は使えません。同じページにある、props が変わったときに一部の state を調整する例では、前の値を state に覚えておき、描画中に比べる書き方が紹介されています。
// AdjustDuringRender.tsx(抜粋。import と Child は省略)
// 一部の state だけを戻したいとき:描画中に前の userId と比べる
export function ProfileAdjust({ userId }: { userId: string }) {
const [comment, setComment] = useState('');
const [rating, setRating] = useState(0); // これは残したい
const [prevUserId, setPrevUserId] = useState(userId);
if (userId !== prevUserId) {
setPrevUserId(userId);
setComment('');
}
log(`描画 ProfileAdjust userId=${userId} comment="${comment}" rating=${rating}`);
return (
<>
<Child label={`comment="${comment}"`} />
<input value={comment} onChange={(e) => setComment(e.target.value)} />
<button id="rate" onClick={() => setRating(rating + 1)}>★ {rating}</button>
</>
);
}
Child は、受け取った label を記録するだけの子のコンポーネントです。コメントを入れ、星を1つ付けてから、userId を変えました。
--- 2c. 描画中に前の userId と比べる(ProfileAdjust) ---
(中略)
> userId を alice から bob に変える
[描画 ProfileAdjust userId=bob comment="すてきな写真" rating=1]
[描画 ProfileAdjust userId=bob comment="" rating=1]
[ 描画 子 comment=""]
input の値: ""
rating のボタン: ★ 1
ProfileAdjust は古いコメントのまま一度呼ばれましたが、子は古いコメントでは描画されず、空のコメントで1回だけ描画されました。星は1のまま残っています。公式の例の説明にあるとおり、描画中に state を更新すると、React は返された JSX を捨てて、子を描画する前にすぐ描画し直します。
ただし、同じ説明には、たいていのコンポーネントはこの書き方も要らないはずだとも書かれています。props から state を調整すると、データの流れが追いにくくなるためです。まず、key ですべて戻せないか、描画中の計算で済まないかを考えます。
リセットボタンで、フォームを初めの状態に戻す
key に渡す値は、ID でなくてもかまいません。useState のリファレンスにある、key で state をリセットする例では、数を key に渡し、ボタンで1つ増やしてフォームを作り直しています。入力欄が多いフォームでも、state を1つずつ戻す処理を書かずに済みます。
// ResetButton.tsx(抜粋。import と SignupForm は省略)
// 「入力をやり直す」で、フォームをまるごと初めの状態に戻す
export function SignupPage() {
const [version, setVersion] = useState(0);
return (
<>
<SignupForm key={version} />
<button id="reset" onClick={() => setVersion(version + 1)}>
入力をやり直す
</button>
</>
);
}
SignupForm は、名前の入力欄と「規約に同意する」のチェックを、それぞれ state に持っています。
--- 3. key={version} で、フォームを初めの状態に戻す ---
入力後: name="neko" 同意=true
> 「入力をやり直す」をクリック
クリック後: name="" 同意=false
描画のたびに変わる値を key にしない
key は、作り直したいときにだけ変わる値にします。描画のたびに変わる値を渡すと、関係のない理由で親が描画し直すたびに、子が作り直されます。key={Math.random()} で試しました。
// RandomKey.tsx(抜粋。import と Note は省略)
// 🔴 描画のたびに key が変わる
export function RandomKeyPage() {
const [count, setCount] = useState(0);
return (
<>
<Note key={Math.random()} />
<button id="other" onClick={() => setCount(count + 1)}>関係のないボタン({count})</button>
</>
);
}
--- 4. key={Math.random()} ---
入力後: note="メモ"
> 「関係のないボタン」をクリック
クリック後: note=""
Note とは関係のないボタンを押しただけで、入力した「メモ」が消えました。
描画のたびに変わる値(Math.random() など)を key にすると、親が描画し直すたびに子の state と DOM が作り直され、入力した内容が消えます。リストの描画のページにある、key の決まりの説明でも、描画中に key を作らないように書かれています。その次の、key が必要な理由の説明にある注意には、こうした key ではコンポーネントと DOM が毎回作り直され、入力が失われるとあります。データの ID のように、作り直したいときだけ変わる値を使います。
前の相手の下書きも残したいとき
key で作り直すと、前の state は捨てられます。検証でも、Alice に戻したときに「こんにちは」は戻りませんでした。相手ごとの下書きを残す方法として、state の保持とリセットのページにある、見えなくなったコンポーネントの state を残す方法の説明では、下書きを親の state に持ち上げる方法や、localStorage に保存する方法などが挙げられています。そのうえで、相手ごとに別の会話なのだから、相手をもとに key を付けるのは理にかなっている、と書かれています。この記事では、下書きを残す方法は試していません。
React は、同じ場所に描画された同じコンポーネントの state を引き継ぎます。key に相手の ID などを渡すと、値が変わったときにそのコンポーネントを作り直し、子まで含めた state と DOM を初めに戻します。エフェクトで空にするより描画が少なく、state が増えても書き足す処理はいりません。key には、作り直したいときだけ変わる値を使います。





