こんにちは。ReactエンジニアのT.C.です。
モードを切り替えてもフォーカスを保持し続ける入力欄を持つコンポーネントを開発していて、挙動を調査したため共有します。
フォーカスが消えるのか消えないのか
問題のコード
以下のAppをレンダリングしたとき、入力欄をダブルクリックしたらフォーカスはどうなるでしょうか?
import { useState } from "react"; type InputProps = { onDoubleClick: () => void; borderColor: "red" | "green"; }; function Input1({ borderColor, onDoubleClick }: InputProps) { return ( <input style={{ border: `4px solid ${borderColor}` }} onDoubleClick={onDoubleClick} /> ); } // コンポーネント名が違うだけでInput1と全く同じ中身 function Input2({ borderColor, onDoubleClick }: InputProps) { return ( <input style={{ border: `4px solid ${borderColor}` }} onDoubleClick={onDoubleClick} /> ); } export default function App() { const [mode, setMode] = useState<"mode1" | "mode2">("mode1"); return ( <main> {mode === "mode1" ? ( <Input1 borderColor={"red"} onDoubleClick={() => { setMode("mode2"); }} /> ) : ( <Input2 borderColor={"green"} onDoubleClick={() => { setMode("mode1"); }} /> )} </main> ); }
正解は、「フォーカスが消える」です。これはReactがInput1とInput2を異なるコンポーネントと認識するため、モードが切り替わった際に古いコンポーネントがアンマウントされ、新しいコンポーネントがマウントされるからです。
追加の調査
では以下のコードではどうでしょうか?
import { useState } from "react"; export default function App() { const [mode, setMode] = useState<"mode1" | "mode2">("mode1"); return ( <main> {mode === "mode1" ? ( <input style={{ border: "4px solid red" }} onDoubleClick={() => { setMode("mode2"); }} /> ) : ( <input style={{ border: "4px solid green" }} onDoubleClick={() => { setMode("mode1"); }} /> )} </main> ); }
こちらは「フォーカスが消えない」です。
Reactが同じ位置に同じ種類のHTML要素がレンダリングされると判断し、既存のDOM要素を再利用するためです。
次の例は以下のコードです。
import { useState } from "react"; type InputProps = { onDoubleClick: () => void; borderColor: "red" | "green"; }; function Input1({ borderColor, onDoubleClick }: InputProps) { return ( <input style={{ border: `4px solid ${borderColor}` }} onDoubleClick={onDoubleClick} /> ); } export default function App() { const [mode, setMode] = useState<"mode1" | "mode2">("mode1"); return ( <main> {mode === "mode1" ? ( <Input1 borderColor={"red"} onDoubleClick={() => { setMode("mode2"); }} /> ) : ( <Input1 borderColor={"green"} onDoubleClick={() => { setMode("mode1"); }} /> )} </main> ); }
こちらは「フォーカスが消えない」です。
こちらもReactが同じ位置に同じ型のコンポーネントがレンダリングされると判断し、既存のコンポーネントインスタンスを再利用するためです。
では、それぞれに別のキーを渡した以下のコードではどうなるでしょうか?
import { useState } from "react"; type InputProps = { onDoubleClick: () => void; borderColor: "red" | "green"; }; function Input1({ borderColor, onDoubleClick }: InputProps) { return ( <input style={{ border: `4px solid ${borderColor}` }} onDoubleClick={onDoubleClick} /> ); } export default function App() { const [mode, setMode] = useState<"mode1" | "mode2">("mode1"); return ( <main> {mode === "mode1" ? ( <Input1 key={"mode1"} borderColor={"red"} onDoubleClick={() => { setMode("mode2"); }} /> ) : ( <Input1 key={"mode2"} borderColor={"green"} onDoubleClick={() => { setMode("mode1"); }} /> )} </main> ); }
こちらは「フォーカスが消える」という結果になります。keyを渡すことでそれぞれのコンポーネントが別のインスタンスとして認識されるからですね。
結論
以上のことから同じコンポーネントへのスイッチングの場合フォーカスが残り、別コンポーネントへのスイッチングの場合フォーカスが消えるということがわかりました。同じコンポーネントとはもう少し正確に言うと、Reactによって同一インスタンスとして扱われるコンポーネントのことです。
この結果は実はkey でフォームをリセットする (React公式)で言及されているstateの状態の保持と同じです。
ちなみに、フォーカスが消える際は、入力欄に入力されていた文字が全てリセットされるのに対し、フォーカスが残る場合は、入力欄に入力されていた文字が全て残っていました。
このように、フォーカスの状態や非制御inputのvalueもkeyでリセットされます。
まとめ
フォーカスが残る条件を調査し、フォーカスが残るかどうかはコンポーネント内部の状態が残るかどうかと同じ条件であることがわかりました。
実際動作を確認すればフォーカスもuseStateで定義した値と同じように保持されるという、いたってシンプルな結果になったのですが、私が実際問題に直面したときすぐに原因がわからなかったので、同じような方の役に立てば嬉しいです。
keyを渡すかどうかで大きく挙動が変わるところでもあるので、しっかり理解して使っていきたいですね。
おわりに
KENTEMでは、様々な拠点でエンジニアを大募集しています! 建設×ITにご興味頂いた方は、是非下記のリンクからご応募ください。 recruit.kentem.jp career.kentem.jp