Reactで1文字ごとに入力欄のフォーカスが外れる原因と直し方
React のフォームで、入力欄に1文字打つとフォーカスが外れ、2文字目から入力できなくなることがあります。親のコンポーネントの中で、入力欄のコンポーネントを定義していると起きます。入力欄のコンポーネントをファイルの一番外側に出し、値と変更の関数を props で渡すと直ります。
検索フォームや問い合わせフォームで、親の state を使いたくて入力欄を親の中で部品にすると、この書き方になります。私は検証環境で、中で定義した場合と、直したあとの書き方を並べて、キーボードで「neko」と打ったときの動きを確かめました。
検証環境は Ubuntu 24.04、Node.js 22.23.4、react / react-dom 19.3.1、TypeScript 6.0.3 です。.tsx を tsc で型チェックしてから esbuild で開発用にまとめ、Playwright 1.63.0 で動かした Chromium 153 で入力しました。マウントのやり直しが記録に混ざらないよう、StrictMode は使っていません。Vite や Next.js の上での動作、Chromium 以外のブラウザ、React Compiler を有効にしたビルドでの動作は試していません。
中で定義した入力欄は、1文字目で止まる
次の NestedForm は、名前を state に持ち、その state を使う入力欄を NameField という部品にしています。NameField は NestedForm の関数の中で定義しています。useEffect の部分は、マウントとアンマウントを記録するための検証用のコードです。この記事のコードでは、import の行を省いています。
export function NestedForm() {
const [name, setName] = useState('');
function NameField() {
useEffect(() => {
log('マウント(NameField)');
return () => log('アンマウント(NameField)');
}, []);
return <input value={name} onChange={(e) => setName(e.target.value)} />;
}
return (
<div>
<NameField />
<p>入力した名前:{name}</p>
</div>
);
}
入力欄をクリックしてから、キーボードで「n」「e」「k」「o」と1文字ずつ打ちました。各行の「フォーカス」は、そのときフォーカスがある要素です。「最初と同じ input 要素か」は、打つ前に入力欄の要素へ目印の属性を書き込んでおき、その目印が残っているかで調べました。
--- NestedForm ---
[マウント(NameField)]
> 入力欄をクリックして、キーボードで "neko" と打つ(1文字ずつ)
[アンマウント(NameField)]
[マウント(NameField)]
「n」のあと: 入力欄="n" / 入力した名前:n / フォーカス=body / 最初と同じ input 要素か=false
「e」のあと: 入力欄="n" / 入力した名前:n / フォーカス=body / 最初と同じ input 要素か=false
「k」のあと: 入力欄="n" / 入力した名前:n / フォーカス=body / 最初と同じ input 要素か=false
「o」のあと: 入力欄="n" / 入力した名前:n / フォーカス=body / 最初と同じ input 要素か=false
1文字目の「n」は入りましたが、その時点でフォーカスは body に移り、残りの3文字はどこにも入りませんでした。入力欄の要素も、最初にクリックしたものとは別の要素に変わっています。
1文字ごとに入力欄をクリックし直すと、4文字とも入りました。そのかわり、1文字打つたびにアンマウントとマウントが記録され、フォーカスは毎回 body に戻りました。次の出力は、最初の2文字の部分だけを抜き出したものです。
「n」のあと: 入力欄="n" / フォーカス=body
[アンマウント(NameField)]
[マウント(NameField)]
「e」のあと: 入力欄="ne" / フォーカス=body
次の GIF は、上に中で定義した NameField、下に外に出した NameField(直し方は後の節で説明します)を並べ、それぞれの入力欄で「neko」と打ったところです。

描画のたびに別のコンポーネントとして作り直される
1文字打つと setName が呼ばれ、NestedForm がもう一度描画されます。このとき function NameField() { … } もあらためて実行されるので、前回とは別の関数ができます。
React は、同じ位置に描画されたものが同じ種類かどうかを、コンポーネントの関数そのもので比べます。関数が別なら、別の種類のコンポーネントとみなし、前の NameField をアンマウントして、新しい NameField をマウントします。公式ドキュメントの、同じ位置で種類が変わると state がリセットされる節では、この書き方を例に挙げ、コンポーネントの関数は一番外側で定義し、入れ子にしないよう勧めています。
アンマウントのときには、state だけでなく DOM の要素も取り除かれます(ESLint のルール static-components の説明)。フォーカスがあった input 要素がページから消え、新しく作られた input にはフォーカスがないので、続けて打った文字はどこにも入りません。検証で body にフォーカスが移り、目印のない input に変わっていたのは、このためです。
外に出して、値と変更の関数を props で渡す
NameField をファイルの一番外側に出します。親の state はそのままでは読めないので、値と、変更のときに呼ぶ関数を props で受け取ります。
type NameFieldProps = {
value: string;
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
};
function NameField({ value, onChange }: NameFieldProps) {
// (マウントを記録する useEffect は省略)
return <input value={value} onChange={onChange} />;
}
export function TopLevelForm() {
const [name, setName] = useState('');
return (
<div>
<NameField value={name} onChange={(e) => setName(e.target.value)} />
<p>入力した名前:{name}</p>
</div>
);
}
--- TopLevelForm ---
[マウント(NameField)]
> 入力欄をクリックして、キーボードで "neko" と打つ(1文字ずつ)
「n」のあと: 入力欄="n" / 入力した名前:n / フォーカス=input / 最初と同じ input 要素か=true
「e」のあと: 入力欄="ne" / 入力した名前:ne / フォーカス=input / 最初と同じ input 要素か=true
「k」のあと: 入力欄="nek" / 入力した名前:nek / フォーカス=input / 最初と同じ input 要素か=true
「o」のあと: 入力欄="neko" / 入力した名前:neko / フォーカス=input / 最初と同じ input 要素か=true
マウントは最初の1回だけで、4文字とも入りました。NameField は一度だけ定義された同じ関数なので、TopLevelForm が描画し直されても同じ種類のままです。input 要素は作り直されず、そのままの要素で value が書き換わります。
コンポーネントの中から親の state を使いたくなったら、中で定義するのではなく、props で渡します。公式ドキュメントでも、中で定義したくなるのは props で渡すべきという合図だ、と説明しています(static-components の説明にある、親の state を使いたいときの注記)。
小さな部品なら、JSX を変数に入れるだけでもよい
1か所でしか使わない入力欄なら、コンポーネントにせず、JSX を変数に入れておく書き方でも直ります。
export function JsxVariableForm() {
const [name, setName] = useState('');
const nameField = <input value={name} onChange={(e) => setName(e.target.value)} />;
return (
<div>
{nameField}
<p>入力した名前:{name}</p>
</div>
);
}
--- JsxVariableForm ---
> 入力欄をクリックして、キーボードで "neko" と打つ(1文字ずつ)
「n」のあと: 入力欄="n" / 入力した名前:n / フォーカス=input / 最初と同じ input 要素か=true
「e」のあと: 入力欄="ne" / 入力した名前:ne / フォーカス=input / 最初と同じ input 要素か=true
「k」のあと: 入力欄="nek" / 入力した名前:nek / フォーカス=input / 最初と同じ input 要素か=true
「o」のあと: 入力欄="neko" / 入力した名前:neko / フォーカス=input / 最初と同じ input 要素か=true
nameField は関数ではなく、描画のたびに作られる <input> の JSX です。React から見ると、同じ位置にいつも input という同じ種類の要素があるので、要素は作り直されません。
useCallback で包んでも直らない
関数が毎回変わるのが原因なら useCallback で同じ関数を使い回せばよい、と考えるかもしれません。入力欄の中で name を読むので、依存配列には name が入ります。
export function UseCallbackForm() {
const [name, setName] = useState('');
const NameField = useCallback(() => {
return <input value={name} onChange={(e) => setName(e.target.value)} />;
}, [name]);
return (
<div>
<NameField />
<p>入力した名前:{name}</p>
</div>
);
}
--- UseCallbackForm ---
> 入力欄をクリックして、キーボードで "neko" と打つ(1文字ずつ)
「n」のあと: 入力欄="n" / 入力した名前:n / フォーカス=body / 最初と同じ input 要素か=false
「e」のあと: 入力欄="n" / 入力した名前:n / フォーカス=body / 最初と同じ input 要素か=false
「k」のあと: 入力欄="n" / 入力した名前:n / フォーカス=body / 最初と同じ input 要素か=false
「o」のあと: 入力欄="n" / 入力した名前:n / フォーカス=body / 最初と同じ input 要素か=false
結果は中で定義したときと同じでした。useCallback のリファレンスにあるとおり、依存配列の値が前回から変わると、useCallback はその描画で渡された新しい関数を返します。1文字打つと name が変わるので、NameField はやはり別の種類のコンポーネントになります。
ESLint の static-components で見つける
eslint-plugin-react-hooks には、コンポーネントの中でコンポーネントを作る書き方を見つける static-components というルールがあり、recommended の設定に入っています。検証で使った 7.1.1 では、次の設定で .tsx を調べました。
import tseslint from 'typescript-eslint';
import reactHooks from 'eslint-plugin-react-hooks';
export default [
{ files: ['src/**/*.tsx'], languageOptions: { parser: tseslint.parser } },
reactHooks.configs.flat.recommended,
];
この記事の4つの書き方を1つのファイルに入れて実行すると、中で定義した NestedForm と、useCallback で包んだ UseCallbackForm の2か所がエラーになりました。外に出した書き方と、JSX を変数に入れた書き方はエラーになっていません。出力の作業ディレクトリのパスは /path/to/app に置き換え、コードの抜き出し部分は一部省略しています。
/path/to/app/src/cases.tsx
19:8 error Error: Cannot create components during render
Components created during render will reset their state each time they are created. Declare components outside of render.
(中略)
71:8 error Error: Cannot create components during render
(中略)
✖ 2 problems (2 errors, 0 warnings)
メッセージは「描画中にコンポーネントを作れない。作られるたびに state がリセットされるので、描画の外で宣言する」という意味です。入力欄のフォーカスが外れる不具合は画面で触らないと気づきにくいので、このルールを有効にしておくと、書いた時点で見つけられます。





