こんにちは。ぬこすけです。
皆さんは React でフォームをライブラリを使わずに実装する時にどう実装しますか?
おそらく useState を使いまくっているのではないかと思います。
少し上級者の方は useReducer を使っているかもしれません。
が、そもそも React で状態管理することなくフォームは実装できます 。
実際にコードをお見せしながら紹介しましょう。
※記事の最後に紹介した全てのコード例を CodeSandbox に載せています。
ありがちな例
コードをお見せする前に、まずはありがちな例から見たいと思います。
(結論のコードだけ知りたい方は読み飛ばして OK です)
簡易的なフォームの例です。
useState を使った実装は次のようになります。
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>
  );
}
サラッと実装内容を見てみましょう。
  const [firstName, setFirstName] = useState("");
  const [lastName, setLastName] = useState("");
  const [question, setQuestion] = useState("");
FirstName と LastName, Question という 3 つの入力フィールドに合わせて、 useState を使って 3 つの状態を作り出しました。
firstName や lastName 、 question は input や textarea タグの value 属性にセットします。
  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 属性にこれらの関数をセットします。
  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 タグでユーザーの入力値に変更があった場合のハンドラを用意しなくてはなりません。
それでは 状態管理を使わない方法を紹介 しましょう。
状態管理せずにフォームを実装
こうなります。
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 を返すだけになりました。
詳しくコードを見てみましょう。
    <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 を使います。
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属性の値]) でユーザーの入力値を参照できる ようになります。
これで終わりです!
ユーザーの入力値に対してリアルタイムで何かしたい場合(例えばリアルタイムでキャッシュしたい場合など)は使えないので注意
(おまけ)状態管理せずにリアルタイムでバリデーションもできるよ
リッチなバリデーションを実装しようと思うと、 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 もちょっとだけいじる必要があるので見てみましょう。
.error-message {
  display: none;
  color: red;
  border: red solid 3px;
}
label:has(input:invalid) .error-message {
  display: block;
}
詳しく説明します。
input タグでは pattern 属性に正規表現を指定できます。
こうすることで、もしユーザーが正規表現にマッチしない文字を入力したときに、 :invalid という疑似要素が form や input タグに付加されます。
input:invalid {
  /* pattern にマッチしない値をユーザーが入力したときに当たるスタイル */
}
label:has(input:invalid) .error-message {
  display: block;
}
さらに :has という疑似要素も駆使します。
これは条件に一致するセレクタにスタイルを与えることができます。
この例では「 input:invalid (=ユーザーが不正な値を入れた input タグ) を含む label タグ内の error-message クラスを持つ要素に display: block; を当てる」ということになります。
このように pattern 属性を使ってユーザーに入力させたい正規表現を定義し、 :has を使ってスタイルを当てることでエラーメッセージを表示 できるわけです。
執筆時点で :has はモダンブラウザでしか使えないため注意
CodeSandbox
今まで載せたコード例は CodeSandbox に載せています。
おまけのリアルタイムでバリデーションも CodeSandbox に載せます。
まとめ
フォームでは実は React で状態管理せずとも実装できる方法をご紹介しました。
この実装によって
- コードの見通しが良くなる
 - 状態変更による再レンダリング抑止など、パフォーマンスも最適化できる
 - 新しく入力フィールドを増やしても実装コストも少なく コードのメンテナンス性も上がる
 
といったメリットがあるかなと思います。
今回は React のフォームの例ですが、エラーの例も紹介しているのでぜひ次の記事も参考にしてみてください!
ここまでご覧いただきありがとうございました! by ぬこすけ

