React Hook Formで数値を処理する際にハマったこと

![logo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123126/d931b5d2-785f-7ffb-a0d6-0077142da840.png)

本記事は、[React Advent Calendar 2021](https://qiita.com/advent-calendar/2021/react)の6日目の記事です。

Reactでフォームを作成するとき、簡単に作成できる[React Hook Form](https://react-hook-form.com/jp/)を使うことがよくあるかと思います。
本記事では、私がReact Hook Formで数値を処理する際にハマったこととその解決方法を紹介します。

なお、記事中のコードは一部分の引用です。記事の最後にCodeSandboxのリンクを記載しています。

# はじめに
React Hook Formでは下記のように`useForm()`を使って必要な関数や値を取得します。
また、フォーム送信のハンドラ(ここでは`onSubmit`)を定義して引数`data`を与えることで、フォームに入力された値を`data`で取得して、フォーム送信時の処理を記載することができます。

```typescript
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`という型を定義しています。

```typescript
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と組み合わせて利用してみます。

作成したフォームはこちらです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123126/1c0ca335-cbd9-e8a0-8533-dce05527f249.png)

コードはこちらです。
下記のように、Controllerコンポーネントを用いることで、React Hook FormとMUIと組み合わせて使うことができます。

```typescript
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>
  );
};
```


より詳細な使い方は本記事では省略させていただきますので、[公式ドキュメント](https://react-hook-form.com/jp/ts)などをご参照ください。

# 発生した問題

今回、私はフォームで金額を入力したかったため、`IFormInput `に`number`の`price`を追加しました。
また、`price`をカンマ区切りで表示するために`toLocaleString()`を使いました。

```typescript
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`の型を出力すると原因が分かりました。

```typescript
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`にして、キャストして使うように修正しました。

```typescript
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()); // カンマ区切りで表示される
  };
```

全体のコードはこちらをご確認ください。
数値以外を入力できないようにバリデーションを追加しています。

https://codesandbox.io/s/number-form-lf0zg?file=/src/index.tsx

以上になります。
最後までお読みいただきありがとうございました。