inputのフォーカス外れ?コンポーネント内のコンポーネント定義のバグ

プロジェクトで、検索フォームコンポーネントを作成していたのですが、テキスト入力欄に文字を入力するたびに、謎のフォーカス外れが発生してました。

入力フォーム「こんに」まで入力 → 「ち」を入力しようとしたらフォーカスが外れている

こんな感じです。ユーザーからしたら単純なバグでは済まない、明らかにUXが損なっている状態でした。


原因の調査

最初はonInputonChangeのハンドラーで何か余計な処理をしているのかと疑いました。しかし、ハンドラーの中身を確認しても、単に状態を更新しているだけです。

const [inputValue, setInputValue] = useState('');

const handleChange = (e) => {
  setInputValue(e.target.value);
};

return <input value={inputValue} onChange={handleChange} />;

おかしいな?と思いながら、React Developer Toolsでコンポーネントの再レンダリングを追跡していると、入力するたびに予期しない再レンダリングが発生していることに気づきました。


原因判明 → コンポーネント内でのコンポーネント定義

怪しいところを見直していると、親コンポーネント内で、こんなコードを書いていました。

function SearchForm() {
  const [inputValue, setInputValue] = useState('');

  // バグの原因
  function InputField() {
    return (
      <input
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
      />
    );
  }

  return (
    <div>
      <InputField />
    </div>
  );
}

InputFieldというコンポーネントを、SearchForm内部で定義していたのです。

これの何が問題かというと、Reactでは以下のような動作をしています:

  1. SearchFormがレンダリングされる
  2. その過程で、毎回新しいInputField関数が生成される
  3. Reactは「新しいコンポーネントだ」と認識してしまう
  4. 既存のDOMノード(input要素)を破棄して、新しく作り直す
  5. その結果、フォーカスが失われる

つまり、「入力値が変わる → SearchFormが再レンダリング → InputFieldが新しく定義される → DOMが再作成される → フォーカスが外れる」という悪循環になってました。


解決方法

解決は簡単です。コンポーネントを、親コンポーネントの外に出して定義します。

// 親コンポーネントの外で定義
function InputField({ value, onChange }) {
  return <input value={value} onChange={onChange} />;
}

function SearchForm() {
  const [inputValue, setInputValue] = useState('');

  return (
    <div>
      <InputField
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
      />
    </div>
  );
}

これで、InputFieldは毎回同じ関数参照になるため、Reactは「このコンポーネントは変わってない」と判断します。その結果、DOMの再作成は行われず、フォーカスも保持されるようになります。


まとめ

親コンポーネント内でコンポーネントを定義してはいけないということを学びました。親がレンダリングされるたびに子コンポーネントが新しく作成されるため、Reactは毎回別のコンポーネント型と判断してしまい、DOM ノードを破棄・再作成してしまいます。だからフォーカスが外されていたわけです。

子コンポーネントを親の外に出すだけで解決したので、シンプルな設計の重要さが分かりました。