Reactのフォームでcontrolled/uncontrolledが混在する実装をどうレビューするか
Reactのフォームでcontrolled/uncontrolledが混在する実装をどうレビューするか
Reactのフォームでは、入力値をstateで管理するcontrolled componentと、DOM側に値を持たせるuncontrolled componentのどちらも使える。
問題は、同じフォーム内で どちらが正なのか分からない状態 になることだ。
レビューで危ないのは、次のような実装である。
valueとdefaultValueが同じ入力に混在している- propsの初期値を
useStateにコピーしたまま更新を追従しない refで読み取る値とReact stateの値が別々に存在する- reset時にDOMとstateの片方だけを戻している
- 外部データ再取得時にフォームの編集状態が上書きされる
この記事ではフォームライブラリ比較ではなく、
入力値の所有者が一貫しているかをレビューで確認する観点を整理する。
まず止めたい実装
type UserFormProps = {
user: User;
onSave: (user: User) => void;
};
export function UserForm({ user, onSave }: UserFormProps) {
const [name, setName] = useState(user.name);
const emailRef = useRef<HTMLInputElement>(null);
return (
<form
onSubmit={event => {
event.preventDefault();
onSave({
...user,
name,
email: emailRef.current?.value ?? user.email,
});
}}
>
<input value={name} onChange={event => setName(event.target.value)} />
<input defaultValue={user.email} ref={emailRef} />
<button type="submit">保存</button>
</form>
);
}@Reviewer: `name` はReact state、`email` はDOM refから取得しており、フォーム値の所有者が分かれています。保存、reset、外部更新時の挙動が揃わないため、フォーム全体でcontrolled/uncontrolledの方針を統一してください。このコードは、保存ボタンを押すだけなら動く。
しかし、user が再取得されたとき、resetしたいとき、バリデーションしたいときに値の所在が分かれる。
レビューでは、入力欄が表示されるかではなく、
フォーム値を誰が持ち、いつ確定するのかを見る必要がある。
なぜ危ないのか
フォームの状態管理が曖昧だと、ユーザー操作の境界で破綻しやすい。
起きやすい問題は次の通りである。
- 初期値が変わっても入力欄が更新されない
- resetしても一部の項目だけ古い値が残る
- バリデーション対象と送信対象の値が違う
- 非同期保存後に画面表示と保存済みデータがずれる
- 編集中の値が再取得データで突然上書きされる
controlledとuncontrolledのどちらが正しいかは状況による。
レビューで重要なのは、1つのフォーム内で責務が混ざっていないことである。
レビューで見たい3つの判断線
1. 入力値の正がReact stateなのかDOMなのか
controlledにするなら、送信、表示、バリデーション、resetの基準はReact stateになる。
DOM refから値を読む処理が混ざっている場合は、理由を確認したい。
@Reviewer: controlledな入力とref読み取りの入力が混在しています。バリデーションや保存時に参照する値がずれるため、React stateを正にするなら全項目をstate管理に寄せてください。2. propsの初期値コピーが同期責務を持っているか
useState(props.value) は初回だけ評価される。
外部の user が変わっても自動では反映されない。
@Reviewer: `user.name` を `useState` の初期値にコピーしていますが、`user` が切り替わった時の同期方針がありません。編集対象IDが変わる場合は、フォーム状態を再初期化する条件を明示してください。ただし、props更新のたびに入力値を上書きすると、編集中の内容を失う。
レビューでは、同期するかどうかだけでなく、どのイベントで再初期化するかを見る。
3. resetとsubmitが同じ状態を見ているか
保存時はstateを見るが、resetはDOM form resetに任せる、といった構造はずれやすい。
resetの責務も、値の所有者に合わせる。
@Reviewer: submitではReact stateを使っていますが、resetはDOMのdefaultValueに依存しています。reset後に送信される値と画面表示が一致するよう、resetも同じ状態管理方針に揃えてください。改善例:controlledに寄せる
編集フォームでバリデーションや保存前確認を行うなら、controlledに寄せるとレビューしやすい。
type UserFormState = {
name: string;
email: string;
};
function toFormState(user: User): UserFormState {
return {
name: user.name,
email: user.email,
};
}
export function UserForm({ user, onSave }: UserFormProps) {
const [form, setForm] = useState(() => toFormState(user));
useEffect(() => {
setForm(toFormState(user));
}, [user.id]);
function updateField(name: keyof UserFormState, value: string) {
setForm(current => ({ ...current, [name]: value }));
}
return (
<form
onSubmit={event => {
event.preventDefault();
onSave({ ...user, ...form });
}}
>
<input
value={form.name}
onChange={event => updateField("name", event.target.value)}
/>
<input
value={form.email}
onChange={event => updateField("email", event.target.value)}
/>
<button type="button" onClick={() => setForm(toFormState(user))}>
リセット
</button>
<button type="submit">保存</button>
</form>
);
}この構造なら、レビューアーは次を確認しやすい。
- フォーム値の正がReact stateに揃っている
- 編集対象IDが変わった時だけ再初期化される
- resetとsubmitが同じ
formを基準にしている - 入力欄ごとの値取得方法が統一されている
- バリデーションを追加する位置が明確である
uncontrolledが妥当なケース
uncontrolledが悪いわけではない。
単純な検索フォームや、送信時だけ値を読めばよいフォームでは妥当なこともある。
export function KeywordSearch({ onSearch }: { onSearch: (keyword: string) => void }) {
const inputRef = useRef<HTMLInputElement>(null);
return (
<form
onSubmit={event => {
event.preventDefault();
onSearch(inputRef.current?.value ?? "");
}}
>
<input ref={inputRef} name="keyword" />
<button type="submit">検索</button>
</form>
);
}@Reviewer: このフォームは送信時だけ値を読み、途中入力をReact stateや外部propsと同期していません。バリデーションや外部更新が不要なら、uncontrolledとして責務が閉じています。重要なのは、uncontrolledならuncontrolledとして閉じることだ。
レビュー観点チェックリスト
- 同じ入力に
valueとdefaultValueが混在していないか - フォーム値の正がReact stateかDOMか明確か
- props初期値をstateへコピーした後の再初期化条件があるか
- resetとsubmitが同じ状態を基準にしているか
- refで読む値とstateで読む値が混在していないか
- 外部データ再取得時に編集中の値を上書きしないか
- controlled/uncontrolledの例外に理由があるか
まとめ
Reactのフォームレビューでは、controlledかuncontrolledかの好みではなく、入力値の所有者を読む。
特に 初期値コピー、reset、submit、外部更新 の4点で状態がずれていないかを確認したい。
フォームは正常系の入力だけなら動いて見える。
レビューでは、編集対象の切り替えや保存失敗後の再編集まで含めて、値の責務が一貫しているかを見る。