React 初心者が Material-UI で今どきの Web フォームを作ってみた(yup編)

React 初心者が、Material-UI と React Hook Form v7 を活用して今どきの Web フォーム開発に挑んでみました!
2021.08.26

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

つい先月、React(+ React Hook Form)と Material-UI を組み合わせた Web アプリ開発を始めました。アプリ開発初心者でも簡単に、かつ今っぽい Web フォームを開発することができたので、少しコードを交えてご紹介してみたいと思います。 なお本記事は、前の記事(react-hook-form編)に続く形式となりますので、お時間あれば下記の記事も合わせてご参照いただけますと幸いです。

作ってみた

前回の記事で作成した Web フォームの基本項目(Basic.js)というフォームに下記の yup という JavaScript schema builder を利用してバリデーションの機能を追加していきたいと思います。

バリデーション機能を追加する

React Hook Form で、yup を活用する際のサンプルコード等が公式ドキュメントに掲載されておりますので、ご参照ください。

まずは、下記のコマンドを利用して yup パッケージをインストールします。

$ npm install @hookform/resolvers yup
$ git diff -p
:
snip
:
diff --git a/package.json b/package.json
index f61e3cb..c8b846f 100644
--- a/package.json
+++ b/package.json
@@ -3,13 +3,17 @@
   "version": "0.1.0",
   "private": true,
   "dependencies": {
+    "@hookform/resolvers": "^2.6.1",
+    "@material-ui/core": "^4.12.2",
     "@testing-library/jest-dom": "^5.14.1",
     "@testing-library/react": "^11.2.7",
     "@testing-library/user-event": "^12.8.3",
     "react": "^17.0.2",
     "react-dom": "^17.0.2",
+    "react-hook-form": "^7.12.1",
     "react-scripts": "4.0.3",
-    "web-vitals": "^1.1.2"
+    "web-vitals": "^1.1.2",
+    "yup": "^0.32.9"

パッケージがインストールされ、package.json に追加されました。それでは、Basic.js のチェックボックスおよび、テキストフィールドに入力を必須とするバリデーション機能を追加していきたいと思います。

Basic.js

:
snip
:
import FormControl from '@material-ui/core/FormControl';
import FormHelperText from "@material-ui/core/FormHelperText";
import { yupResolver } from '@hookform/resolvers/yup';
import * as Yup from 'yup';

function Basic(props) {
    const basicSchema = Yup.object().shape({
        checkBox: Yup.boolean()
            .oneOf([true], 'チェックが必要です'),
        textBox: Yup.string()
            .required('必須項目です')
        pullDown: Yup.string()
            .oneOf(['one', 'two', 'three'], 'いずれかを選択してください'),
    });  
    const { control, handleSubmit, formState:{ errors } } = useForm({
        mode: 'onBlur',
        defaultValues: {
            checkBox: false,
            textBox: "",
            pullDown: "",
        },
        resolver: yupResolver(basicSchema)
    });
    :
    snip
    :
    return (
        <Grid container>
            <Grid sm={2}/>
            <Grid lg={8} sm={8} spacing={10}>
                <form onSubmit={handleSubmit(onSubmit)}>
                    <Controller
                        control={control}
                        name="checkBox"
                        render={({ field: { value, onChange } }) => (
                            <FormControl error>
                                <FormControlLabel
                                    control={
                                        <Checkbox
                                            checked={value}
                                            onChange={onChange}
                                            color='primary'
                                        />
                                    }
                                    label="チェックボックス"
                                />
                                <FormHelperText>
                                    { errors.checkBox?.message }
                                </FormHelperText>
                            </FormControl>
                        )}
                    />
                    <Controller
                        control={control}
                        name="textBox"
                        render={({ field }) => (
                            <TextField
                                {...field}
                                label="テキストフィールド"
                                error={errors.textBox ? true : false}
                                helperText={errors.textBox?.message}
                                fullWidth
                                margin="normal"
                                placeholder="プレースホルダー"
                            />
                        )}
                    />
                    <Controller
                        control={control}
                        name="pullDown"
                        render={({ field }) => (
                            <TextField
                                {...field}
                                label="プルダウンリスト"
                                error={errors.pullDown ? true : false}
                                helperText={errors.pullDown?.message}
                                fullWidth
                                margin="normal"
                                id="select"
                                select
                            >
                                <MenuItem value="one">選択肢1</MenuItem>
                                <MenuItem value="two">選択肢2</MenuItem>
                                <MenuItem value="three">選択肢3</MenuItem>
                            </TextField>
                        )}
                    />

ブラウザから基本項目のフォームを表示させ、おもむろに「次へ」ボタンをクリックしてみます。

1-4-640x450.png

「次へ」のボタンをクリックしたにも関わらず、入力が必須化されているため任意項目のフォームへ遷移することができませんでした。ソースコードの定義としては、下記の実装部分が該当します。oneOf() の第2引数や、required() の引数がエラーメッセージになります。

Basic.js

    const basicSchema = Yup.object().shape({
        checkBox: Yup.boolean()
            .oneOf([true], 'チェックが必要です'),
        textBox: Yup.string()
            .required('必須項目です')
        pullDown: Yup.string()
            .oneOf(['one', 'two', 'three'], 'いずれかを選択してください'),
    });

上記の basicSchema を react-hook-form で利用するために useForm() の resolver に設定することで、外部の検証用ライブラリを利用することができるようになります。

Basic.js

    const { control, handleSubmit, formState:{ errors } } = useForm({
        mode: 'onBlur',
        defaultValues: {
            checkBox: false,
            textBox: "",
            pullDown: "",
        },
        resolver: yupResolver(basicSchema)
    });

エラーメッセージを表示する部分は、下記のように実装しています。

Basic.js

                    <Controller
                        control={control}
                        name="checkBox"
                        render={({ field: { value, onChange } }) => (
                            <FormControl error>
                                <FormControlLabel
                                    control={
                                        <Checkbox
                                            checked={value}
                                            onChange={onChange}
                                            color='primary'
                                        />
                                    }
                                    label="チェックボックス"
                                />
                                <FormHelperText>
                                    { errors.checkBox?.message }
                                </FormHelperText>
                            </FormControl>
                        )}
                    />

チェックボックス側は、FormControl コンポーネントと FormHelperText コンポーネントを利用しています。 各 API の詳細については、Material-UI の公式ドキュメントを参照ください。

チェックボックスとは異なり、テキストフィールドやプルダウンリストは2行(error/helperText プロパティ)追加するだけで対応できました。

Basic.js

                    <Controller
                        control={control}
                        name="textBox"
                        render={({ field }) => (
                            <TextField
                                {...field}
                                label="テキストフィールド"
                                error={errors.textBox ? true : false}
                                helperText={errors.textBox?.message}
                                fullWidth
                                margin="normal"
                                placeholder="プレースホルダー"
                            />
                        )}
                    />
                    <Controller
                        control={control}
                        name="pullDown"
                        render={({ field }) => (
                            <TextField
                                {...field}
                                label="プルダウンリスト"
                                error={errors.pullDown ? true : false}
                                helperText={errors.pullDown?.message}
                                fullWidth
                                margin="normal"
                                id="select"
                                select
                            >
                                <MenuItem value="one">選択肢1</MenuItem>
                                <MenuItem value="two">選択肢2</MenuItem>
                                <MenuItem value="three">選択肢3</MenuItem>
                            </TextField>
                        )}
                    />

こちらも、API の詳細については Material-UI の公式ドキュメントを参照ください。

せっかくなので、テキストフィールドはいくつか制限を設けてみたいと思います。まずは、入力内容を「半角英数字記号」に制限してみます。

Basic.js

    const basicSchema = Yup.object().shape({
        :
        snip
        :
        textBox: Yup.string()
            .required('必須項目です')
            .matches(/^[a-zA-Z0-9!-/:-@¥[-`{-~ ]*$/, "半角英数字記号以外は使用できません")
        :
        snip
        :
    });

matches() の第1引数で指定した正規表現にマッチする必要があるという制限を設けたため、全角文字を入力した場合など、下記のようにエラーとして扱われます。

2-2-640x474.png

次に、文字数制限も追加してみましょう。

Basic.js

    const basicSchema = Yup.object().shape({
        checkBox: Yup.boolean()
            .oneOf([true], 'チェックが必要です'),
        textBox: Yup.string()
            .required('必須項目です')
            .max(10, '10文字以内で入力してください')
            .matches(/^[a-zA-Z0-9!-/:-@¥[-`{-~ ]*$/, "半角英数字記号以外は使用できません")
        :
        snip
        :
    });

max() の第1引数で指定した数値以内の文字数でない場合、エラーとして扱われます。

3-3-640x474.png

良さそうですね。これで、テキストフィールドは「半角英数字記号」かつ「10文字以内」の入力を必須化することができました。

ここまで見てきたとおり、直感的かつ簡単にバリデーション機能を導入することができました。yup には、ここで紹介した他にも色々な機能が用意されておりますので、詳細等については公式の API ドキュメントをご参照ください。

入力確認画面の送信ボタンを実装する

Web フォームとして利用するために、入力確認画面の送信ボタンをクリックした際に外部の API エンドポイントへ入力された内容(JSON 情報)を POST できるようにします。具体的には、Confirm.js の onSubmit() に手を加えます。

変更前

Confirm.js

    const onSubmit = () => {
        alert(JSON.stringify(currentState));
    };

前回の記事でも紹介しておりましたが、入力確認画面の送信ボタンをクリックした際の動作は currentState の JSON データをアラートで表示しているだけでした。そこで、下記のように修正してみます。

変更後

Confirm.js

    const onSubmit = () => {
        postData();
    };
    async function postData() {
        const res = await fetch(
            'https://example.com/api', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(currentState)
            }
        );
    }

修正後は、入力確認画面の送信ボタンをクリックした際に currentState の JSON データを https://example.com/api という架空の API エンドポイントへ POST リクエストを行う非同期処理に変更しています。本番利用する際は、実際に入力された情報を処理するためのシステム等で利用される HTTP エンドポイント(URL)へ置き換える必要があります。

データ送信に失敗した際のエラー通知を追加する

イメージとしては、こんな感じです。

4-2-640x450.png

こちらのエラー通知は、react-hot-toast というライブラリを利用しています。

下記のコマンドを利用して react-hot-toast パッケージをインストールしましょう。

$ npm install react-hot-toast

Confirm.js に少し改修を加えます。

Confirm.js

:
snip
:
import toast, { Toaster } from 'react-hot-toast';
:
snip
:
function Confirm(props) {
    const { currentState } = useContext(UserInputData);
    const notifyError = () => toast.error('データの送信に失敗しました。少し待ってからリトライしてください');
    const onSubmit = () => {
        postData()
        .then(data => {
            console.log(JSON.stringify(data));
        })
        .catch(err => {
            notifyError();
            console.log(err);
        });
    };
    async function postData() {
        const res = await fetch(
            'https://example.com/api', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(currentState)
            }
        );
        const data = await res.json();
        return data;
    }
    :
    snip
    :
    return (
        <Grid container>
            <Toaster position="top-right" duration="4000" />
            <TableContainer component={Paper}>

変更点は、ざっくり下記の5つです。

  • react-hot-toast を import
  • toast() API を呼び出す notifyError 関数を定義
  • postData 関数で、fetch したレスポンスデータを JSON 形式で返却
  • onSubmit 関数内で、postData() 呼び出しがエラーの場合に notifyError() を実行
  • Toaster コンポーネントを配置

わずかな修正で、エラー通知が実装できました。toast() API 等の詳細については、公式ドキュメントをご参照ください。

最後の仕上げとして、データ送信成功時のありがとうページを追加したいと思います。

データ送信が成功した際のサンクスページを追加する

まずは、ソースファイルを用意しましょう。

$ touch src/components/Thanks.js

Thanks.js を全文引用しておきます。

Thanks.js

import { Grid } from '@material-ui/core'
import Typography from '@material-ui/core/Typography';

function Thanks() {
    return (
        <Grid container alignItems="center" justifyContent="center">
            <Typography variant="h4">
                ありがとうございました
            </Typography>
        </Grid>
    )
}

export default Thanks

Confirm コンポーネントのプロパティとして handleNext を渡します。

Content.js

function getStepContent(stepIndex, handleNext, handleBack) {
    switch (stepIndex) {
        case 0:
            return <Basic handleNext={handleNext} />;
        case 1:
            return <Optional handleNext={handleNext} handleBack={handleBack} />;
        case 2:
            return <Confirm handleNext={handleNext} handleBack={handleBack} />;            
        default:
            return 'Unknown stepIndex';
    }
}

postData() が成功した際に、props.handleNext() を呼び出します。

Confirm.js

function Confirm(props) {
    :
    const onSubmit = () => {
        postData()
        .then(data => {
            console.log(JSON.stringify(data));
            props.handleNext();
        })

activeStep と steps.length が同じ場合に Thanks コンポーネントを表示させるように条件分岐を追加しています。

Content.js

:
snip
:
import Thanks from "./Thanks";
    :
    snip
    :
    return (
        <Grid container>
            <Grid sm={2}/>
            <Grid lg={8} sm={8} spacing={10}>
                <Stepper activeStep={activeStep} alternativeLabel>
                    {steps.map((label) => (
                        <Step key={label}>
                            <StepLabel>{label}</StepLabel>
                        </Step>
                    ))}
                </Stepper>
                {activeStep === steps.length ? (
                    <Thanks />
                ) : (
                    <UserInputData.Provider value={value}>
                        { getStepContent(activeStep, handleNext, handleBack)}
                    </UserInputData.Provider>
                )}
            </Grid>
        </Grid>
    )

データ送信処理が成功した場合、下記のようなページが表示されます。

5-2-640x450.png

これで、Web フォームアプリ(サンプル)が完成しました。

さいごに

記事内で紹介したソースコードをベースに少し手を加えたものを GitHub リポジトリで公開しました。

react-hook-form を活用した Web フォームの動作確認(サンプル)や、React 開発入門の題材として、ご活用いただければ幸いです。

ではでは