Chapter 05

Auth:ユーザー作成処理(SignUp):ステップ3

masalib
masalib
2020.12.21に更新

SingUpの処理を画面にバリデーションをいれる

FirebaseのAuthのSignupの処理の本筋とは離れる部分なので飛ばしてもらっても問題ないです。

react-hook-formとは

フォームのバリデーション(入力チェック)をするためのライブラリーです。 最小のパターンだと以下の形です

import React from "react";
import ReactDOM from "react-dom";
import { useForm } from "react-hook-form";
import "./index.css";

function App() {
  const { register, handleSubmit, errors } = useForm();
  const onSubmit = (data) => {
    console.log(JSON.stringify(data));
  };
  return (
    <div className="App">
      <form onSubmit={handleSubmit(onSubmit)}>
        <div>
          <label htmlFor="firstName">First Name</label>
          <input
            name="firstName"
            placeholder="First Name"
            ref={register({ required: true })}
          />
          {errors.firstName?.type === "required" && (
            <div style={{ color: "red" }}>何か文字を入力してください</div>
          )}
        </div>
        <input type="submit" />
      </form>
    </div>
  );
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

react-hook-formをインストール

$ cd ProjectFolder
$ npm install react-hook-form

react-hook-formを導入する

プログラム全文(長文なので注意)
import React, { useReducer, useEffect, useState } from "react";
+ import { useForm } from "react-hook-form";
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField";
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import CardActions from "@material-ui/core/CardActions";
import CardHeader from "@material-ui/core/CardHeader";
import Button from "@material-ui/core/Button";
import { useAuth } from "../contexts/AuthContext";

const useStyles = makeStyles((theme: Theme) =>
  createStyles({
    container: {
      display: "flex",
      flexWrap: "wrap",
      width: 400,
      margin: `${theme.spacing(0)} auto`
    },
    signupBtn: {
      marginTop: theme.spacing(2),
      flexGrow: 1
    },
    header: {
      textAlign: "center",
      background: "#212121",
      color: "#fff"
    },
    card: {
      marginTop: theme.spacing(10)
    }
  })
);

//state type
type State = {
  email: string,
  password: string,
  passwordconfirm: string,
  isButtonDisabled: boolean,
  helperText: string,
  isError: boolean
};

const initialState: State = {
  email: "",
  password: "",
  passwordconfirm: "",
  isButtonDisabled: true,
  helperText: "",
  isError: false
};

type Action =
  | { type: "setEmail", payload: string }
  | { type: "setPassword", payload: string }
  | { type: "setPasswordConfirm", payload: string }
  | { type: "setIsButtonDisabled", payload: boolean }
  | { type: "signupSuccess", payload: string }
  | { type: "signupFailed", payload: string }
  | { type: "setIsError", payload: boolean };

const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case "setEmail":
      return {
        ...state,
        email: action.payload
      };
    case "setPassword":
      return {
        ...state,
        password: action.payload
      };
    case "setPasswordConfirm":
      return {
        ...state,
        passwordconfirm: action.payload
      };
    case "setIsButtonDisabled":
      return {
        ...state,
        isButtonDisabled: action.payload
      };
    case "signupSuccess":
      return {
        ...state,
        helperText: action.payload,
        isError: false
      };
    case "signupFailed":
      return {
        ...state,
        helperText: action.payload,
        isError: true
      };
    case "setIsError":
      return {
        ...state,
        isError: action.payload
      };
    default:
      return state;
  }
};

const Signup = () => {
  const classes = useStyles();
  const [state, dispatch] = useReducer(reducer, initialState);
  const { signup } = useAuth();
  const [error, setError] = useState("");
  const [successMessage, setSuccessMessage] = useState("");
+   const { register, handleSubmit, errors, trigger } = useForm();

  useEffect(() => {
+     if (state.password.trim() !== state.passwordconfirm.trim()) {
+       //clearErrors()
+       dispatch({
+         type: "setIsButtonDisabled",
+         payload: true
+       });
+     } else if (state.email.trim() && state.password.trim()) {
      dispatch({
        type: "setIsButtonDisabled",
        payload: false
      });
    } else {
      dispatch({
        type: "setIsButtonDisabled",
        payload: true
      });
    }
  }, [state.email, state.password, state.passwordconfirm]);

-   async function handleSignup(event) {
-     event.preventDefault();
+   async function handleSignup(data) {
+     //react-hook-formを導入したためevent -> dataに変更
+     //event.preventDefault();   //react-hook-formを導入したため削除

    try {
      setError("");
      setSuccessMessage("");
      //sing up ボタンの無効化
      dispatch({
        type: "setIsButtonDisabled",
        payload: true
      });

      await signup(state.email, state.passwordconfirm);
      dispatch({
        type: "signupSuccess",
        payload: "Signup Successfully"
      });

      //sing up ボタンの有効化
      dispatch({
        type: "setIsButtonDisabled",
        payload: false
      });
      setSuccessMessage("アカウントの作成に成功しました");
    } catch (e) {
      console.log(e);
      //エラーのメッセージの表示
      switch (e.code) {
        case "auth/network-request-failed":
          setError(
            "通信がエラーになったのか、またはタイムアウトになりました。通信環境がいい所で再度やり直してください。"
          );
          break;
        case "auth/weak-password": //バリデーションでいかないようにするので、基本的にはこのコードはこない
          setError("パスワードが短すぎます。6文字以上を入力してください。");
          break;
        case "auth/invalid-email": //バリデーションでいかないようにするので、基本的にはこのコードはこない
          setError("メールアドレスが正しくありません");
          break;
        case "auth/email-already-in-use":
          setError(
            "メールアドレスがすでに使用されています。ログインするか別のメールアドレスで作成してください"
          );
          break;
        case "auth/user-disabled":
          setError("入力されたメールアドレスは無効(BAN)になっています。");
          break;
        default:
          //想定外
          setError(
            "アカウントの作成に失敗しました。通信環境がいい所で再度やり直してください。"
          );
      }
      //sing up ボタンの有効化
      dispatch({
        type: "setIsButtonDisabled",
        payload: false
      });
    }
  }

  const handleKeyPress = (event: React.KeyboardEvent) => {
    if (event.keyCode === 13 || event.which === 13) {
-       state.isButtonDisabled || handleSignup();
+       if (!state.isButtonDisabled) {
+         handleKeyPresstrigger();
+         if (errors) {
+           //errorメッセージを表示する
+         } else {
+           handleSignup();
+         }
+       }
    }
  };

+   async function handleKeyPresstrigger() {
+     const result = await trigger();
+     return result;
+   }

  const handleEmailChange: React.ChangeEventHandler<HTMLInputElement> = (
    event
  ) => {
    dispatch({
      type: "setEmail",
      payload: event.target.value
    });
  };

  const handlePasswordChange: React.ChangeEventHandler<HTMLInputElement> = (
    event
  ) => {
    dispatch({
      type: "setPassword",
      payload: event.target.value
    });
  };

  const handlePasswordConfirmChange: React.ChangeEventHandler<HTMLInputElement> = (
    event
  ) => {
    dispatch({
      type: "setPasswordConfirm",
      payload: event.target.value
    });
  };

  return (
    <form className={classes.container} noValidate autoComplete="off">
      <Card className={classes.card}>
        <CardHeader className={classes.header} title="Sign UP " />
        <CardContent>
          <div>
            {error && <div variant="danger">{error}</div>}
            {successMessage && <div variant="danger">{successMessage}</div>}
            <TextField
              error={state.isError}
              fullWidth
              id="email"
+               name="email"
              type="email"
              label="Email"
              placeholder="Email"
              margin="normal"
              onChange={handleEmailChange}
              onKeyPress={handleKeyPress}
+               inputRef={register({
+                 pattern: /^[A-Za-z0-9]{1}[A-Za-z0-9_.-]*@{1}[A-Za-z0-9_.-]{1,}\.[A-Za-z0-9]{1,}$/
+               })}
            />
+             {errors.email?.type === "pattern" && (
+               <div style={{ color: "red" }}>
+                 メールアドレスの形式で入力されていません
+               </div>
            )}

            <TextField
              error={state.isError}
              fullWidth
              id="password"
+               name="password"
              type="password"
              label="Password"
              placeholder="Password"
              margin="normal"
              helperText={state.helperText}
              onChange={handlePasswordChange}
              onKeyPress={handleKeyPress}
              inputRef={register({ required: true, minLength: 6 })}
            />
+             {errors.password?.type === "minLength" && (
+               <div style={{ color: "red" }}>
+                 パスワードは6文字以上で入力してください
+               </div>
+             )}

            <TextField
              error={state.isError}
              fullWidth
              id="password-confirm"
 +              name="password-confirm"
              type="password"
              label="Password-confirm"
              placeholder="Password-confirm"
              margin="normal"
              helperText={state.helperText}
              onChange={handlePasswordConfirmChange}
              onKeyPress={handleKeyPress}
            />
          </div>
          もしアカウントがあるなら Log In
        </CardContent>
        <CardActions>
          <Button
            variant="contained"
            size="large"
            color="secondary"
            className={classes.signupBtn}
-             onClick={handleSignup}
+             onClick={handleSubmit(handleSignup)}
            disabled={state.isButtonDisabled}
          >
            Signup
          </Button>
        </CardActions>
      </Card>
    </form>
  );
};

export default Signup;

submitの部分にかぶせる

フォームでサブミットしている部分に「handleSubmit」をいれる

- onClick={handleSignup}
+ onClick={handleSubmit(handleSignup)}

submitで動かす処理でイベント(event)を修正

-     async function handleSignup (event) {
-         event.preventDefault()      
+     async function handleSignup (data) {  //react-hook-formを導入したためevent -> dataに変更
+       //event.preventDefault()      //react-hook-formを導入したため削除

inputにバリデーションの部分を追加する

  <TextField
        error={state.isError}
        fullWidth
        id="email"
+       name="email"
        type="email"
        label="Email"
        placeholder="Email"
        margin="normal"
        onChange={handleEmailChange}
        onKeyPress={handleKeyPress}
+       inputRef={register({pattern: /^[A-Za-z0-9]{1}[A-Za-z0-9_.-]*@{1}[A-Za-z0-9_.-]{1,}\.[A-Za-z0-9]{1,}$/ })}
  />

nameも設定していなかったので設定しました

エラー時メッセージを表示

{errors.email?.type === "pattern" &&
<div style={{ color: "red" }}>メールアドレスの形式で入力されていません</div>}

サブミット以外で強制的にエラーハンドリングする場合

Enterキーで動くサブミット処理にもいれてみたのですが
うまくエラーハンドルしてくれない・・・

      const handleKeyPress = (event: React.KeyboardEvent) => {
          if (event.keyCode === 13 || event.which === 13) {
            console.log("handleKeyPress 13 enter")
           state.isButtonDisabled || handleSubmit(handleSignup());
          }
      }

どうやら同期処理じゃないと失敗します。handleKeyPress内の一部を同期処理する事にした

const handleKeyPress = (event: React.KeyboardEvent) => {
    if (event.keyCode === 13 || event.which === 13) {
      console.log("handleKeyPress 13 enter")

      if (!state.isButtonDisabled){
        handleKeyPresstrigger()
        if (errors) {
          //errorメッセージを表示する
        } else {
          handleSignup()  
        }
      }
    }
};
async function handleKeyPresstrigger () {
    const result = await trigger();
    return result
}

triggerは現在入っている内容でエラーハンドリングを起動する関数です。
もしエラーならerrorsに内容が入ってtrueになりSignUpに行きません。

@material-uiに入れる場合の注意点

@material-uiはinputタグを直接に書かない。
そのため「ref」のattribute(属性)が効かない。

終了時点のソース

この画面でもユーザーを作成する事ができます。
React Developer Toolsならユーザーが作成された事が確認できます

この時点のソースです