1
/
5

Reactのライブラリを駆使して、それっぽいアンケートフォームを作ろう

はじめに

Reactには開発を便利にするための様々なライブラリがあります。

今回はフォーム作成に欠かせないFormikと、キレイめなUIを作ってくれるMaterial-UIを使って実用的なフォームを作成する方法を確認します。

長編です。

今回のゴール

以下のようなフォームを作成します。

バリデーションも設定します。

送信後にはメッセージも表示させます。

一見すると複雑な処理が必要に思えますが、ライブラリを使うことでサクッと作ることができます。

事前準備

実行環境

本記事においては以下を使用しています。
React:18.2.0
TypeScript:5.2.2
Vite:5.2.2
Node.js:18.20.2

ライブラリのインストール

FormikとMaterial-UIのインストールを行います。
また、上記に加えてバリデーションで使用するYupというライブラリもインストールします。
※Yupについては今回は詳細な解説はしません。後続でコードのみ紹介します。

npm install formik @mui/material @emotion/react @emotion/styled @mui/x-date-pickers date-fns@2.30.0 yup

ライブラリの概要

Formikはフォーム管理のためのReactのライブラリです。
フォームの値の状態管理、エラー時のバリデーションとエラーメッセージ、submit時の処理
を簡単に扱うことができます。

Material-UIはGoogleのマテリアルデザインを取り入れたReactのコンポーネントライブラリです。
見た目のきれいなボタンや入力フォームなどがあらかじめコンポーネントの形で用意されています。

これら2つを組み合わせることで機能的、かつ見た目の良いフォームを作成することができます。

テキスト用のフォームを作成する

テキスト用フォームのコードは以下の通りとなります。

/* FormikTextField.tsx */

import { TextField } from "@mui/material";
import { useField } from "formik";

interface Props {
name: string;
label: string;
[x: string]: any;
}

const FormikTextField = ({ name, label, ...props }: Props) => {
const [field, meta] = useField(name);
const errorText = meta.error && meta.touched ? meta.error : "";
return (
<TextField
{...field}
{...props}
label={label}
error={!!errorText}
helperText={errorText}
/>
);
};

export default FormikTextField;

ポイントは2つあります。

useFieldの使用

useFieldはフィールドのnameを指定することで配列を返します。
この配列には3種類のオブジェクトが含まれており、そのうちの2つを使っています。
field:フォームに設定する属性(name, value, onChange関数, onChange関数など)
meta:フォームのメタデータ(error, touched, initialValueなど)

TextFieldの使用

TextFieldはその名の通りテキスト用のフォームを構築するためのコンポーネントです。

通常はvalueやonChangeなどのフィールドの状態を設定しますが、useFieldのfieldオブジェクトをTextFieldに設定することで煩雑な処理(stateを用意するなど)を自分で書く必要がなくなります。

また、metaオブジェクトから取得したエラー情報を設定することで、エラーハンドリングも可能になります。

日付用のフォームを用意する

日付用のフォームのコードは以下の通りとなります。

/* FormikDatePicker.tsx */
import { TextField } from "@mui/material";
import { DatePicker } from "@mui/x-date-pickers";
import { useField } from "formik";

interface Props {
name: string;
label: string;
}

const FormikDatePicker = ({ name, label }: Props) => {
const [field, meta, helpers] = useField(name);
const errorText = meta.error && meta.touched ? meta.error : "";
return (
<DatePicker
label={label}
value={field.value}
onChange={(date) => helpers.setValue(date)}
slots={{
textField: TextField,
}}
slotProps={{
textField: {
error: !!errorText,
helperText: errorText,
},
}}
/>
);
};

export default FormikDatePicker;

ポイントは3つです。

DatePickerの使用

DatePickerは日付を選択するためのコンポーネントです。

MUI coreと呼ばれる基本的なMUIのコンポーネントとは別で、MUI Xと呼ばれるより複雑で高度なコンポーネントを扱うことができるものの一つになります。

valueとonChangeの直接指定

TextFieldでは、fieldオブジェクトの値をそのままコンポーネントに設定していました。

しかし、DatePickerのonChangeとfieldオブジェクトが提供するonChangeには互換性がないため、fieldオブジェクトを直接使用することができません。

そのため、value、onChangeについてはそれぞれ個別に指定しています。

slots、slotPropsの使用

DatePickerにはhelperText属性が存在しません。

そのため、slotsとslotPropsを使って、コンポーネントの特定部分の書き換えとpropsの受け渡しを行います。

これにより、DatePickerの内部の部品をTextFieldに置き換え、helperTextを使ってエラー時のメッセージを指定することができます。

ラジオボタンのフォームを用意する

ラジオボタン用のフォームのコードは以下となります。

/* FormikRadioGroup.tsx */
import React from "react";
import { useField } from "formik";
import {
Radio,
RadioGroup,
FormControlLabel,
FormControl,
FormLabel,
FormHelperText,
} from "@mui/material";

interface Option {
label: string;
value: string;
}

interface FormikRadioGroupProps {
name: string;
label: string;
options: Option[];
}

const FormikRadioGroup: React.FC<FormikRadioGroupProps> = ({
name,
label,
options,
}) => {
const [field, meta] = useField(name);
const errorText = meta.touched && meta.error ? meta.error : "";

return (
<FormControl error={!!errorText}>
<FormLabel>{label}</FormLabel>
<RadioGroup {...field}>
{options.map((option) => (
<FormControlLabel
key={option.value}
value={option.value}
control={<Radio />}
label={option.label}
/>
))}
</RadioGroup>
<FormHelperText>{errorText}</FormHelperText>
</FormControl>
);
};

export default FormikRadioGroup;

ポイントは1つです。

RadioGroup、FormControlLabel、Radioの使用

ラジオボタンを生成するにはこの3つのコンポーネントをセットで使う必要があります。

RadioGroup
複数のラジオボタンをグループ化し、グループ内での選択状態を管理できるようにします。
ここではuseFieldのfieldをpropとして設定することで、状態管理を行います。

FormControlLabel
ラジオボタンのほか、チェックボックスなどに使用するコンポーネントです。
ラベルと入力項目の紐づけを行います。
control属性に、実際に表示させる要素を指定します。

Radio
ラジオボタン本体のコンポーネントです。Radio単体でも使うことはできますが、基本的にはFormControlLabelとセットで使用します。

型、バリデーション、初期値の設定

ここまでで、各入力項目の設定は完了しました。
ここから、入力項目全体を取りまとめるコンポーネントを作成しますが、その前にフォーム全体の型、バリデーション、初期値を設定します。

/* types.ts */
import * as Yup from "yup";

// フォームの型
export interface FormValues {
personalInfo: {
name: string;
birthDate: Date | null;
gender: string;
};
}

// バリデーション
export const validationSchema = Yup.object().shape({
personalInfo: Yup.object().shape({
name: Yup.string().required("名前は必須です"),
birthDate: Yup.date().nullable().required("生年月日は必須です"),
gender: Yup.string().required("性別を選択してください"),
}),
});

// 初期値
export const initialValues: FormValues = {
personalInfo: {
name: "",
birthDate: null,
gender: "",
},
};

定数の設定

ラジオボタンに設定する性別のリストを定数として別ファイルに定義しておきましょう。

/* constants.ts */
export const GENDERS = [
{ label: "男性", value: "male" },
{ label: "女性", value: "female" },
{ label: "その他", value: "other" },
];

フォーム要素全体の管理

設定した各フォーム要素を取りまとめるコンポーネントを作成します。

/* UserFormProps.tsx */
import React from "react";
import { Formik, Form } from "formik";
import { Grid, Paper, Typography, Stack, Button } from "@mui/material";
import { FormValues, validationSchema, initialValues } from "./types";
import { GENDERS } from "./constants";
import FormikDatePicker from "./FormikDatePicker";
import FormikTextField from "./FormikTextField";
import FormikRadioGroup from "./FormikRadioGroup";

interface UserFormProps {
onSubmit: (values: FormValues) => Promise<void>;
}

const UserForm: React.FC<UserFormProps> = ({ onSubmit }) => (
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={onSubmit}
>
{({ isSubmitting }) => (
<Form>
<Grid container spacing={3} marginTop={2}>
<Grid item xs={12}>
<Typography variant="h4" gutterBottom>
プロフィールフォーム
</Typography>
</Grid>
<Grid item xs={12}>
<Paper elevation={3}>
<Stack spacing={2} padding={3}>
<Typography variant="h5" gutterBottom>
ユーザー情報
</Typography>
<FormikTextField
name="personalInfo.name"
label="名前"
fullWidth
/>
<FormikDatePicker
name="personalInfo.birthDate"
label="生年月日"
/>
<FormikRadioGroup
name="personalInfo.gender"
label="性別"
options={GENDERS}
/>
</Stack>
</Paper>
</Grid>
<Grid item xs={12}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={isSubmitting}
>
送信
</Button>
</Grid>
</Grid>
</Form>
)}
</Formik>
);

export default UserForm;

ポイントは2つです。

Formik、Formの使用

Formikはフォーム管理を簡素化するためのコンポーネントで、初期値、バリデーション、フォーム送信処理などを定義することができます。

ここでは、types.tsで定義した初期値とバリデーションを設定しています。

Formはhtmlタグの<form>タグのラッパーコンポーネントで、実際のform要素を形成します。

スタイル定義のコンポーネントの使用

Material-UIにはレイアウトを整えるためのコンポーネントが数多く用意されています。
全てを紹介するのは難しいですが、今回使用したコンポーネントについて、簡単に紹介します。

Grid
レスポンシブなレイアウトを作成するためのコンポーネントです。
画面をグリッドとして12分割し、そのグリッドに対して要素を配置します。
containerプロパティを持つのが親グリッド、itemプロパティを持つのが子グリッドになります。
spacingプロパティはグリッド間の間隔を設定できます。
xs={12}は全ての画面サイズで12グリッド分の幅で表示することを示しています。

Typography
テキストのスタイリングの一貫性を保つためのコンポーネントです。
variantプロパティでテキストの種類を指定します。
gutterBottomは下部にmarginを追加するプロパティです。

Paper
浮き上がったような外観を持つ表面を作成するコンポーネントです。
evaluationプロパティで影の深さを設定します。

Stack
子要素を垂直または水平に配置するためのコンポーネントです。
spacingプロパティで子要素間の間隔を設定できます。
paddingプロパティで内部の余白を設定します。

アプリ全体の管理

フォームの設定も終わったので最終仕上げです。

/* App.tsx */
import { useState } from "react";
import { Container, Snackbar, Alert } from "@mui/material";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import { ja } from "date-fns/locale";
import UserForm from "./UserForm";
import { FormValues } from "./types";

const App = () => {
const [snackbar, setSnackbar] = useState({
open: false,
message: "",
severity: "success" as "success" | "error",
});

const handleSubmit = async (values: FormValues) => {
try {
// フォーム送信処理を擬似的に表現
await new Promise((resolve) => setTimeout(resolve, 100));
console.log(values); // フォームの値をログ出力
setSnackbar({
open: true,
message: "フォームが正常に送信されました!",
severity: "success",
});
} catch (error) {
setSnackbar({
open: true,
message: "エラーが発生しました。もう一度お試しください。",
severity: "error",
});
}
};

const handleSnackbarClose = () => {
setSnackbar({ ...snackbar, open: false });
};

return (
<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={ja}>
<Container maxWidth="sm">
<UserForm onSubmit={handleSubmit} />
</Container>

<Snackbar
open={snackbar.open}
autoHideDuration={6000}
onClose={handleSnackbarClose}
>
<Alert
onClose={handleSnackbarClose}
severity={snackbar.severity}
sx={{ width: "100%" }}
>
{snackbar.message}
</Alert>
</Snackbar>
</LocalizationProvider>
);
};

export default App;

ポイントは3つです。

submit処理の定義

UserFormコンポーネントに渡すsubmit処理を定義しました。
ここでは、擬似的なsubmit処理と、スナックバーの表示を制御しています。

DatePickerの日本語化

LocalizationProviderコンポーネントを使うことで、DatePickerのローカライズができます。
デフォルトでは英語になっているので、adapterLocaleで日本語を指定します。

Snackbar、Alertの使用

Snackbarは画面の下部に一時的なメッセージを表示することができるコンポーネントです。
送信後の結果や、エラーなどの重要な通知を表示するのに適しています。

Alertはメッセージを目立たせるためのコンポーネントです。
severityにアラートの種類(success、error、warning、info)を指定することで、それぞれに適したスタイルのメッセージが表示されます。

ここまででフォーム作成のための実装は完了です。

まとめ

Formikは、useFieldでフォームの状態管理に必要なオブジェクトを取得できたり、Formikコンポーネントでフォーム全体の管理をすることができたりと、フォーム管理に関するライブラリとして使いやすいなと感じました。

Material-UIも、あまりデザインの知識がなくとも項目に合ったコンポーネントを当てるだけで整った見た目のフォームになりました。

TextFieldやDatePicker、Radioなど直接的な画面項目に関するコンポーネントが用意されていたり、Paper、Grid、Stackなど見た目を整えるためのコンポーネントも豊富で拡張もしやすそうでしたね。

これらを駆使することで、今以上に「それっぽいフォーム」を作成することができそうです。

株式会社JOINT CREWからお誘い
この話題に共感したら、メンバーと話してみませんか?
株式会社JOINT CREWでは一緒に働く仲間を募集しています
2 いいね!
2 いいね!

同じタグの記事

今週のランキング

吉田 光輔さんにいいねを伝えよう
吉田 光輔さんや会社があなたに興味を持つかも