uwu

プログラミングの備忘録を書いています。誰かの為になれば幸いです

React + MUI + TS + React-hook-formで作るログインフォーム

今回作ったもの



デザインはMUIのテンプレお借りしました。Tyvm💛
9+ Free React Templates - Material UI


もちろん、会員登録ページなどにもお使いいただけます。


指定したバリデーションに引っかかるとこんな感じになります。





今回はvalidationライブラリを使っていません。
ちなみに緑の鍵マークは使っているセキュリティソフトのアイコンです。


環境

Windows11
React18
TypeScript4.7
MUI5.1

React-hook-formのインストール方法
// npm を使う場合
npm install react-hook-form

// yarn を使う場合
yarn install react-hook-form
コード

まずは完成形のコードを貼っていきます。


フォームの使いまわしができるように
コンポーネントに分けました。


コンポーネント:

import { memo } from 'react';
// React-hook-form
import { useForm, SubmitHandler } from 'react-hook-form';

// MUI
import {
  Avatar,
  CssBaseline,
  Box,
  Typography,
  Container,
  Button,
} from '@mui/material';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';

// Components
import Input from '../Atoms/FormInputText';

// Input form type
export type FormValues = {
  email: string;
  password: string;
};

const LoginForm = memo(() => {
  const { handleSubmit, control } = useForm<FormValues>();

  // Validation rules for login form
  const validationRules = {
    email: {
      required: 'Email is required',
      pattern: {
        value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
        message: 'Please enter a valid email',
      },
      maxLength: { value: 254, message: 'Email is too long' },
    },
    password: {
      required: 'Password is required',
      minLength: { value: 6, message: 'Password is too short' },
    },
  };

  const onSubmit: SubmitHandler<FormValues> = (data: FormValues) => {
    console.log(`data: ${JSON.stringify(data)}`);
  };

  return (
    <Container component='main' maxWidth='xs'>
      <CssBaseline />
      <Box
        sx={{
          marginTop: 8,
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
        }}
      >
        <Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
          <LockOutlinedIcon />
        </Avatar>
        <Typography component='h1' variant='h5'>
          Log in
        </Typography>
        <Box component='form' noValidate sx={{ mt: 1 }}>
          {/* Input for email */}
          <Input
            name={'email'}
            rules={validationRules.email}
            required={true}
            id={'email'}
            type={'email'}
            label={'Email Address'}
            autoComplete={'email'}
            autoFocus={true}
            control={control}
          />
          {/* Input for password */}
          <Input
            name={'password'}
            rules={validationRules.password}
            required={true}
            id={'password'}
            type={'password'}
            label={'Password'}
            autoComplete={'password'}
            autoFocus={true}
            control={control}
          />
          {/* Submit button */}
          <Button
            onClick={handleSubmit(onSubmit)}
            fullWidth
            variant='contained'
            sx={{ mt: 3, mb: 2 }}
          >
            LOG IN
          </Button>
        </Box>
      </Box>
    </Container>
  );
});

export default LoginForm;


コンポーネント:

import { FC, memo } from 'react';
import { Controller } from 'react-hook-form';

// MUI
import TextField from '@material-ui/core/TextField';

// Form type
import { FormInputProps } from '../Types/Form/FormInputProps';

const FormInputTmp: FC<FormInputProps> = memo((props) => {
  const {
    name,
    control,
    rules,
    required,
    type,
    id,
    label,
    autoComplete,
    autoFocus,
  } = props;

  return (
    <Controller
      name={name}
      control={control}
      defaultValue={''}
      rules={rules}
      render={({
        field: { onChange, value },
        fieldState: { error },
        formState,
      }) => (
        <TextField
          margin='normal'
          fullWidth
          variant='outlined'
          required={required}
          type={type}
          id={id}
          label={label}
          name={name}
          autoComplete={autoComplete}
          autoFocus={autoFocus}
          error={!!error}
          onChange={onChange}
          value={value}
          helperText={error ? error.message : null}
        />
      )}
    />
  );
});

export default FormInputTmp;


型宣言したファイル:
any警察に捕まりませんように@@

type InputFiledType = 'text' | 'password' | 'email';

export interface FormInputProps {
  name: string;
  control: any;
  label: string;
  setValue?: any;
  required?: boolean;
  type?: InputFiledType;
  id?: string;
  autoComplete?: string;
  autoFocus?: boolean;
  error?: boolean;
  helperText?: string | undefined;
  rules: any;
}
コンポーネントの説明


まず、react-hook-formからuseFormSubmitHandlerをインポートします。


import { useForm, SubmitHandler } from 'react-hook-form';


useFormは「インプット、セレクト要素のデータをreact-hook-formに登録」したり、
「入力されたデータをリセット」したりしてくれる
便利なメソッドが入ったオブジェクトを返します。



メソッドの種類はこちらから:
useForm | React Hook Form - Simple React forms validation



SubmitHandlerはTypeScript で書く場合に必要な型定義です。 
submit イベントに関連して実行する関数の型宣言に使います。



次に、入力値の型宣言をします。


export type FormValues = {
  email: string;
  password: string;
};

次に、useFromから、handleSubmit, controlを定義します。
useForm フックの直後に、フォームの入力値についての型を宣言します。


 const { handleSubmit, control } = useForm<FormValues>();

handleSubmitは、
ログインボタンを押した時、フォーム データを受け取るためのもの、
controlにはコンポーネント
React Hook Formに登録するためのメソッドが入っています。


次に、バリデーションのルールを定義します。
今回はemailには入力必須、emailの形式で、254文字までというルール、
passwordには入力必須、6文字以上というルールをつけています。


// Validation rules for login form
  const validationRules = {
    email: {
      required: 'Email is required',
      pattern: {
        value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
        message: 'Please enter a valid email',
      },
      maxLength: { value: 254, message: 'Email is too long' },
    },
    password: {
      required: 'Password is required',
      minLength: { value: 6, message: 'Password is too short' },
    },
  };

利用できるバリデーションルールは以下です。


  • required
  • min
  • max
  • minLength
  • maxLength
  • pattern
  • validate

validateを使うとカスタムルールを設定することができます。



次に、子コンポーネント(フォーム)に必要な情報を渡します。

        {/* Input for email */}
          <Input
            name={'email'}
            rules={validationRules.email}
            required={true}
            id={'email'}
            type={'email'}
            label={'Email Address'}
            autoComplete={'email'}
            autoFocus={true}
            control={control}
          />
          {/* Input for password */}
          <Input
            name={'password'}
            rules={validationRules.password}
            required={true}
            id={'password'}
            type={'password'}
            label={'Password'}
            autoComplete={'password'}
            autoFocus={true}
            control={control}
          />
          {/* Submit button */}
          <Button
            onClick={handleSubmit(onSubmit)}
            fullWidth
            variant='contained'
            sx={{ mt: 3, mb: 2 }}
          >
            LOG IN
          </Button>

ボタンをクリックすると、handleSubmit()が実行され
バリデーションokだったら自分で定義したonSubmit関数が呼ばれます。


handleSubmit は、第 1引数の関数を「バリデーションokだった場合」に実行します。
「バリデーション🙅‍♀️」だった場合、第2引数にダメだった時の関数を渡すことができるみたいです。


呼ばれる関数は以下です。
SubmitHandlerに続けて、フォーム入力値の型を宣言しています。
引数のdataには、フォーム入力値のデータがオブジェクトとして格納されています。便利。


  const onSubmit: SubmitHandler<FormValues> = (data: FormValues) => {
    console.log(`data: ${JSON.stringify(data)}`);
  };
コンポーネント


Controllerをreact-hook-formからインポートします。

import { Controller } from 'react-hook-form';

ControllerはMUIなどの為のラッパーコンポーネントです。
これで要素をラップしてあげることで要素を制御できます。


ちなみに、reactt-hook-formで要素を制御するには2つの方法があって、
ControllerRegisterというものがあります。


Controllerを使うとフォーム入力時に再レンダリングするので
基本的にはregisterの使用がおすすめされています。


ただ、MUIを使用する場合すでにMUIによって要素が制御されてるので
react-hook-formのControllerを使うことになります。



Controllerに関するDoc:
Controller | React Hook Form - Simple React forms validation



コードの説明に戻ります。
コンポーネントから必要な情報をpropsで受け取って、
それぞれのpropを渡しています。


   <Controller
      name={name}
      control={control}
      defaultValue={''}
      rules={rules}
      render={({ field: { onChange, value }, fieldState: { error } }) => (
        <TextField
          margin='normal'
          fullWidth
          variant='outlined'
          required={required}
          type={type}
          id={id}
          label={label}
          name={name}
          autoComplete={autoComplete}
          autoFocus={autoFocus}
          error={!!error}
          onChange={onChange}
          value={value}
          helperText={error ? error.message : null}
        />
      )}

nameは必須です。これがないと判別してくれません。
rules={rules}は、親コンポーネントで定義したバリデーションルール
control={control}react-hook-formにコンポーネントを登録するメソッド
defaultValue={''}で、初期レンダーの際に呼ばれる初期値を設定できます。
render={}


これはイベントと値をに渡してくれるrender propです。
この関数は、field、fieldState、formStateの3つを持っています。



fieldStateのerrorにはバリデーションがダメだった時のメッセージが格納されます。



バリデーションがダメだった時はのerrorでエラーの状態を切り替え、
helperTextにエラーメッセージを入れることでエラーを表示しています。


     error={!!error}
     helperText={error ? error.message : null}

コードの説明は以上となります。
お付き合いいただきありがとうございました!