こんにちは。ぬこすけです。
皆さんは 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 ぬこすけ