React Hook FormとMUIを組み合わせて入力フォームを作ろうと思いました。
しかし、エラーが上手く表示されず四苦八苦…。
調べてみたところ下記の記事にたどり着き、順調に実装を進めていました。
しかし、MUIのTextFieldを使い、そのコンポーネントのtypeがnumberの時に
再度詰まったので、ここにメモしておきます。
一言まとめ
なんで詰まったか?
- Reactのcontrolled・uncontrolledコンポーネントを理解していなかったから
- MUIとReact Hook Formが上記のどちらを採用しているのか知らなかったから
- React Hook FormにはController(controlled)とregister(uncontrolled)の2種類があることを理解していなかったから
結論:MUIのTextFieldを使う時はregisterでOK
- タイトル通り、MUIのTextFieldを使う時は{…register}
- それ以外はControllerでコンポーネントをラップする
一番はじめに理解すべきこと
何よりもまず理解することがあります。
それはReactを使ってフォームを実装する際、デザインパターンが2つあるということです。
controlled component(制御コンポーネント)
- 自身で状態を保持しており、ユーザーの入力に基づいて状態を更新する。
- 値の更新はuseState()を用いる。
- stateの更新が行われるので、再レンダリングされる。
uncontrolled component(非制御コンポーネント)
- データはDOM自身が保持する。
- Reactはrefを利用して、DOMから値を取得する。
- defaultValue 属性(初期値)を指定することができる。
→これを指定すると、設定した値がフォーム要素にあらかじめ入力された状態で初回のレンダリングが行われる。
defaultChecked属性(あらかじめチェックをつけておく)もある。 - レンダリングが走らないので、パフォーマンスが良い
特別な理由がない限り、制御コンポーネントを使うことを公式はオススメしています。
公式ドキュメント:https://ja.reactjs.org/docs/forms.html#controlled-components
MUIとReact Hook Formのコンポーネントは?
- MUIの場合:controlled・uncontrolledどちらの使い方もできる
公式ドキュメント:https://mui.com/material-ui/react-text-field/#uncontrolled-vs-controlled - React Hook Formの場合:基本的にuncontrolledな使い方しかできない
公式ドキュメント:https://react-hook-form.com/api/usecontroller/controller/
React Hook Formでcontrolledなコンポーネントを扱いたい場合は、
Controllerというラッパーコンポーネントを使う必要があります。
React Hook Formのバリデーションには2種類の定義方法がある
Controller
controlledなフィールドとしてコンポーネントを定義するために用いるラッパーコンポーネント。
(MUIなどの)controlledなコンポーネントを扱う場合、
Controllerコンポーネントでラップする必要があります。
//例。controlledなMUIのコンポーネントをラップしています。
<Controller
control={control}
name="residence"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<>
<TextField
{...field}
select
fullWidth
label="居住地"
error={!!error?.message}
helperText={error?.message}
variant="standard"
>
{RESIDENCE.map((residence) => (
<MenuItem
key={residence}
value={residence}
{...register("residence", { required: "選択必須項目です" })}
>
{residence}
</MenuItem>
))}
</TextField>
</>
)}
/>
register
uncontrolledなフィールドとしてコンポーネントを定義するために用いる関数。
//例。こちらは属性としてコンポーネントに渡す。
<TextField
label="年齢"
error={!!errors?.age}
helperText={errors?.age?.message}
type="number"
{...register("age", {
required: "入力して",
minLength: { value: 2, message: "2桁以内で入力してください" },
maxLength: { value: 2, message: "2桁以内で入力してください" },
})}
/>
TextFieldを使う時にControllerでラップすると、defaultValueを設定しなければならない
TextFieldはMUIのコンポーネントです。inputタグにレンダリングされます。
先ほどお話した通り、Controllerでラップすることでcontrolledなフィールドとして定義できます。
MUIのTextFieldはcontrolled・uncontrolledどちらの使い方もできます。
なのでControllerでラップしてcontrolledなフィールドとして定義することも、
TextFieldとregisterを組み合わせてuncontrolledなフィールドとして定義することも可能です。
(記事冒頭でリンクさせていただいた参考記事では、他のコンポーネントと合わせて
Controllerでラップする方向でお話されていました。)
しかし、Controllerでラップする場合defaultValue={}が必要です。
(defaultValue={}を指定しないとエラーが出ます)
(ここが私もきちんと理解できているのか不安なのですが、
Controller自身はuncontrolledなコンポーネントだから
defaultValue={}が必要なのだと思います。)
でもdefaultValue={}に値を設定すると、その値が
あらかじめ入力された状態でレンダリングされます。
こんな感じです。年齢の入力フィールドにあらかじめ1が入力されています。
あらかじめ入力された値(↑の場合は1)はユーザー自身で削除して
自分が入力したい値を入力する必要があります。
うざいですよね…。
これがテキストの場合は、defaultValue={“”}とすることで解決できます。
が、type=numberの場合はそうはいきません。型エラーが出ますよね。
もちろん、なにも値を渡さなかった場合もエラーが出ます。
undefindやnullを渡すと、一見なにもエラーが出ないように見えますが、
ユーザーが何か入力した時にエラーが出ます。
これは「非制御コンポーネントを制御コンポーネントのように扱ってはいけない」
的な感じで怒られています。
よく見ると、
defaultValue={1}を設定すると、inputタグにvalue属性が追加され
あらかじめ1が入っていますね。
試しになにか文字を入力してみると、入力する度にこの値が
更新されることがわかります。(是非確かめてみてください!)
しかし、defaultValueにundefindやnullを入れると
valueと書かれているだけで、何も値が設定されていません。
(当たり前っちゃ当たり前ですが…。)
何か入力してみても、valueが更新されることはありません。
このことからも、なぜdefaultValueを設定しないとエラーが出るのか?
が理解できると思います。
該当箇所のコード
<Controller
control={control}
name="age"
// ↓省略するとエラーが出る
defaultValue={1}
rules={{
required: { value: true, message: "入力必須項目です" },
minLength: { value: 2, message: "2桁以上で入力してください" },
maxLength: { value: 2, message: "2桁以内で入力してください" },
}}
render={({ field, fieldState: { error } }) => (
<TextField
{...field}
label="年齢"
error={!!error?.message}
helperText={error?.message}
type="number"
variant="standard"
/>
)}
/>
解決方法:強制的に型の情報を無効にする
<Controller
control={control}
name="age"
// ↓型情報を強制的に無効にしないと、型エラーが出る
// @ts-ignore
defaultValue={""}
rules={{
required: { value: true, message: "入力必須項目です" },
minLength: { value: 2, message: "2桁以上で入力してください" },
maxLength: { value: 2, message: "2桁以内で入力してください" },
}}
render={({ field, fieldState: { error } }) => (
<TextField
{...field}
label="年齢"
error={!!error?.message}
helperText={error?.message}
type="number"
variant="standard"
// inputProps={{ minLength: 2, maxLength: 4 }}
/>
)}
/>
// @ts-ignoreを指定して型情報を強制的に無効にすると、エラーは消えます。
これでもまぁ良いのですが、これではTypeScriptで開発している意味がありません。
その他の解決方法を調べましたが、見当たりませんでした。
なので、他のラジオボタンやセレクトボックスと合わせて
inputフィールドもControllerでラップしたいという場合は
defaultValue={}を設定する他なさそうです。
それでは!
コメント