【React】フォームは状態管理せずに実装できるよ

こんにちは。[ぬこすけ](https://twitter.com/nuko_suke_dev)です。

皆さんは React でフォームをライブラリを使わずに実装する時にどう実装しますか？
おそらく `useState` を使いまくっているのではないかと思います。

少し上級者の方は `useReducer` を使っているかもしれません。
が、そもそも **React で状態管理することなくフォームは実装できます** 。

実際にコードをお見せしながら紹介しましょう。

※記事の最後に紹介した全てのコード例を CodeSandbox に載せています。

## ありがちな例

コードをお見せする前に、まずはありがちな例から見たいと思います。
（結論のコードだけ知りたい方は読み飛ばして OK です）

簡易的なフォームの例です。

![スクリーンショット 2023-01-13 10.40.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164159/b2251feb-92de-e6a3-acfe-3cbfb93d8da8.png)

`useState` を使った実装は次のようになります。

```react
import { useState, ChangeEventHandler, FormEventHandler } from "react";

export default function UseStateForm() {
  const [firstName, setFirstName] = useState("");
  const [lastName, setLastName] = useState("");
  const [question, setQuestion] = useState("");
  const handleFirstNameChange: ChangeEventHandler<HTMLInputElement> = ({
    target
  }) => {
    setFirstName(target.value);
  };
  const handleLastNameChange: ChangeEventHandler<HTMLInputElement> = ({
    target
  }) => {
    setLastName(target.value);
  };
  const handleQuestionChange: ChangeEventHandler<HTMLTextAreaElement> = ({
    target
  }) => {
    setQuestion(target.value);
  };
  const handleSubmit: FormEventHandler<HTMLFormElement> = (event) => {
    event.preventDefault();
    alert(
      `FirstName: ${firstName}\nLastName: ${lastName}\nQuestion: ${question}`
    );
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        FirstName:
        <input type="text" value={firstName} onChange={handleFirstNameChange} />
      </label>
      <label>
        LastName:
        <input type="text" value={lastName} onChange={handleLastNameChange} />
      </label>
      <label>
        Question:
        <textarea value={question} onChange={handleQuestionChange} />
      </label>
      <input type="submit" value="Submit" />
    </form>
  );
}
```

サラッと実装内容を見てみましょう。

```react
  const [firstName, setFirstName] = useState("");
  const [lastName, setLastName] = useState("");
  const [question, setQuestion] = useState("");
```

FirstName と LastName, Question という 3 つの入力フィールドに合わせて、 `useState` を使って 3 つの状態を作り出しました。

`firstName` や `lastName` 、 `question` は `input` や `textarea` タグの `value` 属性にセットします。

```react
  const handleFirstNameChange: ChangeEventHandler<HTMLInputElement> = ({
    target
  }) => {
    setFirstName(target.value);
  };
  const handleLastNameChange: ChangeEventHandler<HTMLInputElement> = ({
    target
  }) => {
    setLastName(target.value);
  };
  const handleQuestionChange: ChangeEventHandler<HTMLTextAreaElement> = ({
    target
  }) => {
    setQuestion(target.value);
  };
```

次に `input` や `textarea` タグでユーザーが文字を入力したときに発火する関数を用意します。
`target.value` にはユーザーが入力した値が入り、先ほどの `useState` で作成した `setXXX` を使って状態を更新します。

`input` や `textarea` タグの `onChange` 属性にこれらの関数をセットします。

```react
  const handleSubmit: FormEventHandler<HTMLFormElement> = (event) => {
    event.preventDefault();
    alert(
      `FirstName: ${firstName}\nLastName: ${lastName}\nQuestion: ${question}`
    );
  };
```

最後に Submit ボタンをタップしたときに発火する関数を用意します。
デフォルトだとフォームのリクエストが走ってしまうので `event.preventDefault()` でデフォルトの挙動を止めます。

`alert` では React で状態管理している `firstName` と `lastName` 、 `question` を表示するわけです。

ここまでありがちな例を紹介しましたが、 `useState` を使うとどうやら実装が面倒そうです。
管理する状態も多いですし、 `input` や `textarea` タグでユーザーの入力値に変更があった場合のハンドラを用意しなくてはなりません。

それでは **状態管理を使わない方法を紹介** しましょう。

## 状態管理せずにフォームを実装

こうなります。

```react
import { FormEventHandler } from "react";

const handleSubmit: FormEventHandler<HTMLFormElement> = (event) => {
  event.preventDefault();
  const form = new FormData(event.currentTarget);
  const firstName = form.get("firstName") || "";
  const lastName = form.get("lastName") || "";
  const question = form.get("question") || "";
  alert(
    `FirstName: ${firstName}\nLastName: ${lastName}\nQuestion: ${question}`
  );
};

export default function NoUseStateForm() {
  return (
    <form onSubmit={handleSubmit}>
      <label>
        FirstName:
        <input type="text" name="firstName" defaultValue="" />
      </label>
      <label>
        LastName:
        <input type="text" name="lastName" defaultValue="" />
      </label>
      <label>
        Question:
        <textarea name="question" defaultValue="" />
      </label>
      <input type="submit" value="Submit" />
    </form>
  );
}
```

スッキリしましたね。
Submit ボタンを押下したときに発火する関数はコンポーネント外で定義できるようになり、コンポーネントは JSX を返すだけになりました。

詳しくコードを見てみましょう。

```jsx
    <form name="noUseStateForm" onSubmit={handleSubmit}>
      <label>
        FirstName:
        <input type="text" name="firstName" defaultValue="" />
      </label>
      <label>
        LastName:
        <input type="text" name="lastName" defaultValue="" />
      </label>
      <label>
        Question:
        <textarea name="question" defaultValue="" />
      </label>
      <input type="submit" value="Submit" />
    </form>
```

こちらの JSX ですが、 `input` や `textarea` に `name` 属性を追加しました。
このあとお話しますが、 `name` 属性を使ってラベリングすることでユーザーの入力値を参照できるようになります。

また、 `value` を使わず `defaultValue` に変更しています。
React では状態管理する場合は `value` を使いますが、状態管理しない場合は `defaultValue` を使います。

```react
const handleSubmit: FormEventHandler<HTMLFormElement> = (event) => {
  event.preventDefault();
  cosnt form = new FormData(event.currentTarget);
  const firstName = form.get("firstName") || "";
  const lastName = form.get("lastName") || "";
  const question = form.get("question") || "";
  alert(
    `FirstName: ${firstName}\nLastName: ${lastName}\nQuestion: ${question}`
  );
};
```

続いて Submit ボタンを押したときの処理です。
先ほど `name` 属性を使ってラベリングしましたが、 `new FormData(event.currentTarget);` でフォームのデータを解釈した後、 **`form.get([name属性の値])` でユーザーの入力値を参照できる** ようになります。
これで終わりです！

:::note warn
ユーザーの入力値に対してリアルタイムで何かしたい場合（例えばリアルタイムでキャッシュしたい場合など）は使えないので注意
:::

## （おまけ）状態管理せずにリアルタイムでバリデーションもできるよ

![スクリーンショット 2023-01-17 22.16.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164159/e32f0119-393d-a75b-81b6-1f604643323c.png)

リッチなバリデーションを実装しようと思うと、 React の状態管理に頼る必要はあるでしょう。
例えばリアルタイムでのバリデーションです。

実は **リアルタイムでのバリデーションも React の状態管理なしで実装できます** 。

コード例を載せます。
（先に紹介したコード例から少し簡略化しています）

```react
import { FormEventHandler } from "react";

const handleSubmit: FormEventHandler<HTMLFormElement> = (event) => {
  event.preventDefault();
  cosnt form = new FormData(event.currentTarget);
  const firstName = form.get("firstName") || "";
  const lastName = form.get("lastName") || "";
  const question = form.get("question") || "";
};

export default function NoUseStateForm() {
  return (
    <form onSubmit={handleSubmit}>
      <label>
        FirstName:
        <input
          type="text"
          name="firstName"
          defaultValue=""
          pattern="[a-zA-Z]"
        />
        <span className="error-message">
          FirstNameは英字で入力してください。
        </span>
      </label>
      <label>
        LastName:
        <input type="text" name="lastName" defaultValue="" pattern="[a-zA-Z]" />
        <span className="error-message">
          LastNameは英字で入力してください。
        </span>
      </label>
    </form>
  );
}
```

`input` タグに `pattern` 属性が増えていますね。

CSS もちょっとだけいじる必要があるので見てみましょう。

```css
.error-message {
  display: none;
  color: red;
  border: red solid 3px;
}

label:has(input:invalid) .error-message {
  display: block;
}
```

詳しく説明します。

`input` タグでは `pattern` 属性に正規表現を指定できます。
こうすることで、もしユーザーが正規表現にマッチしない文字を入力したときに、 `:invalid` という疑似要素が `form` や `input` タグに付加されます。

```css
input:invalid {
  /* pattern にマッチしない値をユーザーが入力したときに当たるスタイル */
}
```

https://developer.mozilla.org/ja/docs/Web/HTML/Attributes/pattern

```css
label:has(input:invalid) .error-message {
  display: block;
}
```

さらに `:has` という疑似要素も駆使します。
これは条件に一致するセレクタにスタイルを与えることができます。

https://developer.mozilla.org/ja/docs/Web/CSS/:has

この例では「 `input:invalid` (=ユーザーが不正な値を入れた `input` タグ) を含む `label` タグ内の `error-message` クラスを持つ要素に `display: block;` を当てる」ということになります。

このように **`pattern` 属性を使ってユーザーに入力させたい正規表現を定義し、 `:has` を使ってスタイルを当てることでエラーメッセージを表示** できるわけです。

:::note warn
執筆時点で `:has` は[モダンブラウザでしか使えない](https://developer.mozilla.org/ja/docs/Web/CSS/:has#%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E3%83%BC%E3%81%AE%E4%BA%92%E6%8F%9B%E6%80%A7)ため注意
:::

## CodeSandbox

今まで載せたコード例は CodeSandbox に載せています。

https://codesandbox.io/embed/blissful-bartik-1v5ve6?fontsize=14&hidenavigation=1&theme=dark

おまけのリアルタイムでバリデーションも CodeSandbox に載せます。

https://codesandbox.io/embed/vigorous-borg-g1jdwb?fontsize=14&hidenavigation=1&theme=dark

## まとめ

フォームでは実は React で状態管理せずとも実装できる方法をご紹介しました。

この実装によって

- **コードの見通しが良くなる**
- 状態変更による再レンダリング抑止など、**パフォーマンスも最適化**できる
- 新しく入力フィールドを増やしても実装コストも少なく **コードのメンテナンス性も上がる**

といったメリットがあるかなと思います。

今回は React のフォームの例ですが、エラーの例も紹介しているのでぜひ次の記事も参考にしてみてください！

https://qiita.com/nuko-suke/items/fdecac831533c3d8bbf0

ここまでご覧いただきありがとうございました！ by [ぬこすけ](https://twitter.com/nuko_suke_dev)