本記事は、React Advent Calendar 2021の6日目の記事です。
Reactでフォームを作成するとき、簡単に作成できるReact Hook Formを使うことがよくあるかと思います。
本記事では、私がReact Hook Formで数値を処理する際にハマったこととその解決方法を紹介します。
なお、記事中のコードは一部分の引用です。記事の最後にCodeSandboxのリンクを記載しています。
はじめに
React Hook Formでは下記のようにuseForm()
を使って必要な関数や値を取得します。
また、フォーム送信のハンドラ(ここではonSubmit
)を定義して引数data
を与えることで、フォームに入力された値をdata
で取得して、フォーム送信時の処理を記載することができます。
const App = () => {
const { control, handleSubmit } = useForm();
const onSubmit = (data) => {
// フォーム送信時の処理を記載する
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{/* ここにフォームを作る */}
<input type="submit" />
</form>
);
};
TypeScriptを使う場合、useForm()
に型を与えてdata
の型を定義することができます。
この場合はIFormInput
という型を定義しています。
interface IFormInput {
firstName: string;
lastName: string;
}
const App = () => {
const { control, handleSubmit } = useForm<IFormInput>(); // 型を定義
const onSubmit = (data: IFormInput) => {
console.log(data);
};
さらに、React Hook FormはMUI(Material-UI)と組み合わせて使うこともできます。
ここでは、MUIのTextFieldと組み合わせて利用してみます。
コードはこちらです。
下記のように、Controllerコンポーネントを用いることで、React Hook FormとMUIと組み合わせて使うことができます。
interface IFormInput {
firstName: string;
lastName: string;
}
const App = () => {
const { control, handleSubmit } = useForm<IFormInput>();
const onSubmit = (data: IFormInput) => {
console.log(data);
};
// Controllerコンポーネントを用いることでMaterial-UIと組み合わせて利用可能
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Stack spacing={2}>
<label>First Name</label>
<Controller
render={({ field }) => <TextField {...field} />}
name="firstName"
defaultValue=""
control={control}
/>
<label>Last Name</label>
<Controller
render={({ field }) => <TextField {...field} />}
name="lastName"
defaultValue=""
control={control}
/>
<input type="submit" />
</Stack>
</form>
);
};
より詳細な使い方は本記事では省略させていただきますので、公式ドキュメントなどをご参照ください。
発生した問題
今回、私はフォームで金額を入力したかったため、IFormInput
にnumber
のprice
を追加しました。
また、price
をカンマ区切りで表示するためにtoLocaleString()
を使いました。
type FormData = {
firstName: string;
lastName: string;
price: number; // 追加
};
const App = () => {
const { control, handleSubmit } = useForm<IFormInput>();
const onSubmit = (data: IFormInput) => {
console.log(data.price.toLocaleString()); // カンマ区切りで表示したい
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Stack spacing={2}>
<label>First Name</label>
<Controller
render={({ field }) => <TextField {...field} />}
name="firstName"
defaultValue=""
control={control}
/>
<label>Last Name</label>
<Controller
render={({ field }) => <TextField {...field} />}
name="lastName"
defaultValue=""
control={control}
/>
{/* 追加 */}
<label>Price</label>
<Controller
render={({ field }) => <TextField {...field} />}
name="price"
defaultValue=""
control={control}
/>
<input type="submit" />
</Stack>
</form>
);
};
しかし、100000
を入力してもコンソールには100000
と出力され、うまく動きませんでした。(期待値は100,000
)
原因を調査したところ、typeof
でprice
の型を出力すると原因が分かりました。
interface IFormInput {
firstName: string;
lastName: string;
price: number;
}
const App = () => {
const { control, handleSubmit } = useForm<IFormInput>();
const onSubmit = (data: IFormInput) => {
console.log(typeof data.price); // string
なんとprice
はstring
になっていました。
なので、toLocaleString()
を使ってもカンマ区切りにはならなかったのです。
IFormInput
でnumber
を指定してすっかり安心してしまっていました。
フォームからの出力なのでよく考えればstring
しか定義できないような気はしますが、number
で指定して特にエラーにもならず、VSCodeの型表示もnumber
となっていたこともあり、ハマってしまいました。
異なる型でも代入できるのは、TypeScriptの型付けの闇な気がします。
修正版
price
はstring
にして、キャストして使うように修正しました。
interface IFormInput {
firstName: string;
lastName: string;
price: string;
}
const App = () => {
const { control, handleSubmit } = useForm<IFormInput>();
const onSubmit = (data: IFormInput) => {
console.log(Number(data.price).toLocaleString()); // カンマ区切りで表示される
};
全体のコードはこちらをご確認ください。
数値以外を入力できないようにバリデーションを追加しています。
以上になります。
最後までお読みいただきありがとうございました。