はじめに
React19の新機能(<form>アクション)を利用すると、checkboxがクリアされてしまうという(不思議な)現象に遭遇しました
Reactの<form>アクション説明ページでは「非制御コンポーネント」をクリアすると明記されています(逆に、制御コンポーネントはクリアしない動作が正しい)
アクションが成功すると、React は非制御コンポーネントの場合にフォームを自動的にリセットします。
2024/12にissue #31695として登録されていますが、その後の進捗はないようです
[React 19] Controlled checkboxes are reset by form submission and form.reset() #31695
※ shadcn/ui の<Checkbox>でも同じ動作になるため、issueがあがっています
再現プログラム
- テスト環境を作成
$ npm create vite@latest react19-action-checkbox -- --template react-ts
$ cd react19-action-checkbox
$ npm install
$ code .
- App.tsxを以下のように修正する
- checkboxとtextboxを2つ用意し、一方は非制御、もう一方は制御コンポーネントとする
- Submitボタンを押下すると<form>アクションを実行する
- <form>を(送信することなく)再描画させるために「count」ボタンを用意する
import { useState } from 'react';
type ActionHandler = (formData: FormData) => void | Promise<void>;
const Form = ({ actionHandler }: { actionHandler: ActionHandler }) => {
const [checked, setChecked] = useState(false);
const [text, setText] = useState('');
return (
<form action={actionHandler}>
<p>
<input type="checkbox" id="checkbox1" />
<label htmlFor="checkbox1">非制御コンポーネント(checkbox)</label>
</p>
<p>
<input
type="checkbox"
id="checkbox2"
checked={checked}
onChange={(e) => setChecked(e.target.checked)}
/>
<label htmlFor="checkbox2">制御コンポーネント(checkbox)</label>
</p>
<p>
<input type="text" id="text1" />
<label htmlFor="text1">非制御コンポーネント(text)</label>
</p>
<p>
<input
type="text"
id="text2"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<label htmlFor="text2">制御コンポーネント(text)</label>
</p>
<button type="submit">Submit</button>
</form>
);
};
function App() {
const [count, setCount] = useState(0);
const actionHandler = async (formData: FormData) => {
console.log('checkbox1', formData.get('checkbox1'));
await fetch('https://httpstat.us/200', {
method: 'POST',
body: JSON.stringify({ count: count + 1 }),
});
setCount((prevCount) => prevCount + 1);
};
return (
<>
<h1>
React19のアクションで、状態管理をしているcheckboxのチェックが消える
</h1>
<div className="card">
<Form actionHandler={actionHandler} />
<br />
<button
onClick={(e) => {
e.preventDefault();
setCount((count) => count + 1);
}}
>
count is {count}
</button>
</div>
</>
);
}
export default App;
再現手順
- checkboxにチェックをつけ、textに適当な値を入力する
- 「Submit」をクリックすると、制御コンポーネント(checkbox)もクリアされてしまう
- chckboxの
state
は保持されているので、コンポーネントが再描画される(countボタンをクリック)とチェック状態に戻る(stateとDOMの状態が不一致な状態)
対応方法
-
action をやめて通常の onSubmit+preventDefault で送信
React18までのやり方を踏襲する
-
action 実行後、<form>の再描画を行う
アクション実行後、<form>のkeyを変更することで再描画を行う。checkboxのstate自体は保持されているため、チェックされた状態に戻る
const [formVersion, forceUpdate] = useState(0);
return (
<form
key={formVersion}
action={(formData) => {
actionHandler(formData);
forceUpdate((prev) => prev + 1);
}}
>
- が無難かと思います。2.もやってみましたが、違和感なく表示されました
追記(2025/06/29)
アロー関数(戻り値無し)の形で呼び出すと、<checkbox>の値がクリアされない(一瞬チェックが消えてから、再度チェックが付く)
- <Form actionHandler={actionHandler} />
+ <form action={(formData) => { actionHandler(formData); }}
returnがないため即時でresetが行われ、その後で再描画が実行されるので値がチェック状態が元に戻る?
- 中かっこを省略すると(暗黙的ににreturnあり)、<checkbox>の値がクリアされる
- <Form actionHandler={actionHandler} />
+ <form action={(formData) => actionHandler(formData)}
こちらは暗黙的にreturnされるので、resetされるタイミングが上記と異なる?
※React側の動作がおかしいような気がします・・・