[React]フォーム入力の度にフォーカスが外れてしまうときに確認すべきこと2選


はじめに

Reactでフォーム入力を扱ってる時,こんな風に入力の度にフォーカスが外れて連続入力ができない現象に遭遇して少しハマったのでまとめました。(以下のGIFで入力のたびに青枠が外れてるのがわかるかと思います)

問題のコード と CodeSandbox

CodeSandbox:
https://codesandbox.io/s/billowing-cherry-fy3dx?file=/src/App.js

import React from "react";

function App() {
  const [inputValue, setInputValue] = React.useState("");

  const handleInput = (event) => {
    event.preventDefault();
    setInputValue(event.target.value);
  };

  const InputForm = () => {
    return (
      <label htmlFor="input2">
        <input
          id="input2"
          type="text"
          defaultValue={inputValue}
          onChange={handleInput}
        />
      </label>
    );
  };

  return (
    <div className="App">
      <InputForm />
    </div>
  );
}

export default App;

原因

子コンポーネントを親コンポーネントのブロックの中で定義していたことが原因でした。

親コンポーネント(App)が再描画される度に、小コンポーネント(InputForm)を再定義、再描画していたので、Reactがもともとフォーカスを当てていたエレメントが破棄され、新しいエレメントとしてのinputが作成されていたためにフォーカスが外れていたようです。

解決方法:

子コンポーネントの定義をコンポーネントの外にすればOKです!
CodeSandbox
https://codesandbox.io/s/input-fixed-9hphl?file=/src/App.js

import React from "react";

function App() {
  const [inputValue, setInputValue] = React.useState("");

  const handleInput = (event) => {
    event.preventDefault();
    setInputValue(event.target.value);
  };

  return (
    <div className="App">
      <InputForm value={inputValue} handler={handleInput} />
    </div>
  );
}

export default App;

//親コンポーネントの外側に定義 別ファイルでも可
const InputForm = (props) => {
  return (
    <label htmlFor="input2">
      <input
        id="input2"
        type="text"
        defaultValue={props.value}
        onChange={props.handler}
      />
    </label>
  );
};

別の原因の可能性

実は原因を調べる過程で、同じ現象が起きる状況が他にもあることがわかりました。
それは mapで複数コンポーネントを描画した際のkeyに描画ごとに変化しうる値を使っている場合です
こちらも一例目と同じように、Reactがフォーカスをあてていたinputのkeyが存在しなくなり、新規エレメントとして描画していることがフォーカスが外れる原因みたいです。

バグコードの例

このコードでは、keyをフォームごとにユニークにするために入力値を用いています。(これだとユニークになるとは限らないですが、一例です)
Codesandbox:
https://codesandbox.io/s/multiple-inputs-with-a-bug-qt7lu?file=/src/App.js

import React from "react";

function App() {
  const [inputValueArr, setInputValueArr] = React.useState([
    "input1",
    "input2",
    "input3"
  ]);

  const handleInput = (event, index) => {
    event.preventDefault();
    const newInputValueArr = [...inputValueArr];
    newInputValueArr[index] = event.target.value;
    setInputValueArr(newInputValueArr);
  };

  return (
    <div className="App">
      {inputValueArr.map((value, index) => (
        <label key={`hoge_${value}`} htmlFor="input1">
          keyにバリューを使っている時
          <input
            id={`input_${value}`}
            type="text"
            defaultValue={value}
            onChange={(e) => handleInput(e, index)}
          />
        </label>
      ))}
    </div>
  );
}

export default App;

修正後の例

上記コードではkeyが入力のたびに変化してしまうので、フォームごとに固定で固有なkeyをつけてあげます。ここではindexを使います。
Codesandbox:
https://codesandbox.io/s/multiple-inputs-fixed-8wcsi?file=/src/App.js

import React from "react";

function App() {
  const [inputValueArr, setInputValueArr] = React.useState([
    "input1",
    "input2",
    "input3"
  ]);

  const handleInput = (event, index) => {
    event.preventDefault();
    const newInputValueArr = [...inputValueArr];
    newInputValueArr[index] = event.target.value;
    setInputValueArr(newInputValueArr);
  };

  return (
    <div className="App">
      {inputValueArr.map((value, index) => (
        <label key={index} htmlFor="input1">
          keyにバリューを使っている時
          <input
            id={`input_${value}`}
            type="text"
            defaultValue={value}
            onChange={(e) => handleInput(e, index)}
          />
        </label>
      ))}
    </div>
  );
}

export default App;

おわりに

最初は浅いコピーやら深いコピーやら絡みのStateの更新の仕方の問題かと思いましたが、もっと初歩的な話でした。公式ドキュメント読んだら書いてあるのかもしれないですね。この記事がどなたかのお役に立てれば幸いです!

参考