Closed5
React Hook Formを使ってsubmit on change
React Hook Form(以下RHF)はフォームの値を管理・バリデーションを支援してくれるライブラリだが、基本的にボタンを押してフォームの値をsubmitするという形式で動いているので、submit on changeをするためには多少の工夫が必要だった。その過程で生じた工夫と疑問点をまとめる
基本的にRHFのリポジトリにあるこちらのディスカッションを参考にしながら、以下のように日付を入力すると日付順でソートが行われるフォームを実装した。この時、日付の入力部分がsubmit on changeになっている。
- Container.tsx
import { FC, useCallback, useState } from 'react';
import { VStack, Text, HStack } from '@chakra-ui/react';
import type { DateID } from 'schemas/time';
import { Day } from 'schemas/time';
import InputForm from './InputForm';
const compare = (a: Day, b: Day): number => {
if (a.toISO8601() === b.toISO8601()) return 0;
else if (a.toISO8601() > b.toISO8601()) return 1;
else return -1;
};
const Container: FC = () => {
const [days, setDays] = useState<Map<DateID, Day>>(
new Map([
[0, new Day(2023, 1, 20)],
[1, new Day(2023, 1, 21)],
[2, new Day(2023, 1, 22)],
])
);
console.log('rendered!');
const setDay = useCallback((id: DateID, day: Day): void => {
setDays((days) => {
const entries = [...days.set(id, day).entries()].sort(([_, a], [__, b]) =>
compare(a, b)
);
return new Map(entries);
});
}, []);
return (
<VStack>
{[...days.entries()].map(([id, day], i) => (
<HStack key={i}>
<Text>{i + 1}:</Text>
<InputForm
key={id}
dateID={id}
defaultDate={day.toISO8601()}
setDay={setDay}
/>
</HStack>
))}
</VStack>
);
};
export default Container;
- InputForm.tsx
import { FC, useEffect } from 'react';
import { FormControl, FormErrorMessage, Input } from '@chakra-ui/react';
import { yupResolver } from '@hookform/resolvers/yup';
import { useForm } from 'react-hook-form';
import { Day } from 'schemas/time';
import type { ISO8601, DateID } from 'schemas/time';
import { dateFormSchema, DateFormSchema } from 'schemas/validation';
const InputForm: FC<{
dateID: DateID;
defaultDate: ISO8601 | undefined;
setDay: (id: DateID, date: Day) => void;
}> = ({ dateID, defaultDate, setDay }) => {
const {
register,
watch,
formState: { errors, isValidating, isValid },
} = useForm<DateFormSchema>({
mode: 'onChange',
defaultValues: { date: defaultDate },
resolver: yupResolver(dateFormSchema),
});
// 入力値に変化があるたびにsubmitする
const date = watch('date');
useEffect(() => {
if (isValid && !isValidating) {
console.log('date is changed!: ', date);
setDay(dateID, Day.fromISO8601(date as ISO8601));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dateID, date, isValid, isValidating]);
return (
<form>
<FormControl margin={5} isInvalid={errors.date !== undefined}>
<Input w="157px" type="date" isRequired {...register('date')} />
<FormErrorMessage>{errors.date?.message}</FormErrorMessage>
</FormControl>
</form>
);
};
export default InputForm;
工夫した点
- InputForm.tsxの
useEffect
の依存配列に日付を更新する関数setDay()
を含めないようにした-
setDay()
はレンダリングごとにアドレスが変わるため、useEffect()
の依存配列に含めてしまうとContainerのstateの更新→useEffect
実行→stateの更新・・・と無限ループが発生する
-
-
useEffect
の依存配列にsetDay()
を含めなくて良いようにするために、useCallback
でsetDay()
を包んだ-
ドキュメントに書かれているように、再レンダリング間で
setState
は同一性が保持されるため、useCallback
の依存配列にはsetDays()
を入れる必要がなくなり、その結果setDay()
についても再レンダリング間で同一性が保たれるようになる
-
ドキュメントに書かれているように、再レンダリング間で
疑問点
- Container.tsxで
setDay()
関数をuseCallback
に包まず、さらにInputFormでsetDay()
関数をuseEffect
の依存配列に含めないという対応をしてもなぜかうまく行く-
setDay()
によってContainerのstateを更新した場合、再レンダリングによってsetDay()
のアドレスが更新されるため、依存配列に含めない場合は古いsetDay()
が参照されてstateの更新がうまくいかないはずでは?
-
このスクラップは5ヶ月前にクローズされました