🍺

Nextjs+ChakraUi+FirebaseAuth+react-hook-formで作るログイン画面(その1:ロジック編)

2024/05/13に公開

概要

Next.js, ChakraUI, react-hook-form と Firebase Authenticationでログイン画面を作成したので、内容をまとめてみようかと思います。

使っているライブラリ関連はこんな感じです。

対象 ライブラリ名
フレームワーク Next.js
スタイル ChakraUI
バックエンド(BaaS) Firebase(Authentication/Firestore)
フォーム react-hook-form
バリデーション yup

最終的にはこんな画面を作ろうかと思います。
Komawariというのは今開発中のアプリ名です。

それなりの量になりますので、何回かに分けてお伝えします。その1はロジック編となります。(ロジック部はTSやJSで記述する部分です)
その1:ロジック編 🔗今ここ
その2:コンポーネント編
その3:パスワードリセット編
その4:ソーシャルログイン編(作成中)

全体のコード

TSX/JSXファイルでは、TS/JSで記述するロジック部分とHTML要素とともに記述するレンダー部分に分かれます。通常は、return文までの部分がロジック部分で、return文の中身がレンダー部分となります。
今回は、ロジック部分について説明します。

全体のコードーは下のリンクをクリックしてください。

ここをクリックしてコードを表示
// react, next
import { useRouter } from "next/router";
import NextLink from 'next/link';
// 3rd party
import {
    Box,
    HStack,
    Link,
    Stack,
} from '@chakra-ui/react';
import {
    Auth,
    getAuth,
    signInWithEmailAndPassword,
} from "firebase/auth";
import { FirebaseError } from 'firebase/app';
import { useForm, SubmitHandler } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
// ganymede
import { Layout } from "../../components/Layout";
import { AccountSubmitButton } from '../../components/atoms/Button';
import { FormCard } from '../../components/organisms/Card';
import { SocialAuth } from "../../components/organisms/SocialAuth";
import { ModalAlert } from "../../components/organisms/Modal";
import { FormPasswordInputControl, FormTextInputControl } from '../../components/organisms/Form';
import { firebaseApp } from '../../firebase/firebase';
import { useErrorHandler } from "../../hooks/useModalHandler";
import { signinSchema } from '../../validation/validationSchemas';


type InputFormData = {
    email: string;
    password: string;
};


const SignIn = () => {
    const auth: Auth = getAuth(firebaseApp);
    const router = useRouter();
    const { isOpen, onClose, errorMessage, errorHandler } = useErrorHandler();

    const formDefaultValues: InputFormData = {
        email: '',
        password: '',
    };
    const {
        register,
        handleSubmit,
        reset,
        formState: { errors },
    } = useForm({
        mode: 'all',
        defaultValues: formDefaultValues,
        resolver: yupResolver(signinSchema),
    });

    const onSubmit: SubmitHandler<InputFormData> = async (data) => {
        try {
            await signInWithEmailAndPassword(auth, data.email, data.password);
            router.push('/');
        } catch (e: unknown) {
            const error = e instanceof FirebaseError ? e : e as Error;
            errorHandler(error);
            reset({ email: '', password: '' });
        }
    };

    return (
        <Layout title="Sign In">
            <FormCard title="サインイン">
                <form onSubmit={handleSubmit(onSubmit)} data-testid="sign-in-form">
                    <Stack spacing={4}>
                        <FormTextInputControl
                            id="email"
                            isInvalid={errors.email ? true : false}
                            placeholder="email"
                            registers={register('email')}           // registerの引数はInputDataのkey
                            errorMessage={errors.email && errors.email.message}
                        >
                            Email
                        </FormTextInputControl>
                        <Box>
                            <FormPasswordInputControl
                                id="password"
                                isInvalid={errors.password ? true : false}
                                placeholder="password"
                                registers={register('password')}    // registerの引数はInputDataのkey
                                errorMessage={errors.password && errors.password.message}
                            >
                                パスワード
                            </FormPasswordInputControl>
                            <Box mt={2}>
                                <Link as ={NextLink} href='/account/pw-reset-mail' color={'blue.400'}>
                                    パスワードをお忘れですか?
                                </Link>
                            </Box>
                        </Box>
                        <HStack alignItems="center" justifyContent="end">
                            <AccountSubmitButton>Sign In</AccountSubmitButton>
                        </HStack>
                        <ModalAlert
                            isOpen={isOpen}
                            modalTitle="Error"
                            onClose={onClose}
                        >
                            {errorMessage}
                        </ModalAlert>
                    </Stack>
                    <Box mt={12}>
                        <SocialAuth>
                            Sign in
                        </SocialAuth>
                    </Box>
                </form>
            </FormCard>
        </Layout>
    );
};


export default SignIn;

コードの説明(ロジック部)

🔖フックなどの実行部

    const auth: Auth = getAuth(firebaseApp);
    const router = useRouter();
    const { isOpen, onClose, errorMessage, errorHandler } = useErrorHandler();
  • getAuthはfirebaseの関数でローカルにある認証情報を取得しています。firebase authenticationを使用するときは大抵このコードが必要になります。firebaseAppは、別モジュールの中で
    firebase/firebase.ts
    export const firebaseApp = initializeApp(firebaseConfig)
    
    で取得しています。firebaseConfigは、
    firebaseのプロジェクトを作成したら生成されるJSONオブジェクトです。(apiKeyとかappIdとかが入っているやつ)
  • useRouterは、Next.jsのカスタムフックで、出力のrouterで、ページ遷移を行います。
  • useErrorHandlerは、独自のエラー用カスタムフックでポップアップ画面(今回はModalAlertコンポーネント)などの制御に使用します。

🔖フォーム初期値設定部

sign-in.tsx
    const formDefaultValues: InputFormData = {
        email: '',
        password: '',
    };

この部分は、フォームの初期値に型を明示しているだけです。useFormのPropsに直接記述しても問題ないです。

🔖react-hook-form実行部

sign-in.tsx
    const {
        register,
        handleSubmit,
        reset,
        formState: { errors },
    } = useForm({
        mode: 'all',
        defaultValues: formDefaultValues,
        resolver: yupResolver(signinSchema),
    });

コードの説明の前に、まずはreact-hook-formとuseFormの説明から


🎈 react-hook-formとuseForm

react-hook-formはFormを管理するためのライブラリです。formikというライブラリもあるようですが、react-hook-formの評判の方が良さげだったので、こちらを採用しました。
公式サイトを見ると、react-hook-formには、6つのAPIが用意されています。今回のログイン画面のような割とシンプルなフォームなら、useFormで十分なようです。
useFromは、react-hook-formのメインとなるカスタムフックです。
初期値などのpropsを入れて、form管理に役立つ関数などを出力します。
https://react-hook-form.com/docs


useFormの入力値

  • mode:
    バリデーションのタイミングを設定する属性です。allonBluronChangeの両方で実行されるようです。その他の情報は、公式を参照して下さい。
  • defaultValues:
    その名の通り初期値です。
  • resolver:
    yupなどの外部のバリデーションライブラリを使用するときに使用します。今回はyupを使用しています。詳細は後述します。

useFormの出力値

  • register
    対象のInput要素などを直接参照したり、フォームフィールドの値の追跡を行なったりできるようになります。バリデーションの設定なども行いますが、今回はyupを使用しているため、バリデーションの記述はなしです。

  • handleSubmit
    フォームの送信用の関数で、form要素のonSubmit属性にセットします。

  • reset
    フォームのフィールドを初期値にリセットするための関数になります。

  • formState
    formの状態を表すオブジェクトです。エラーは送信状態などが含まれる模様
    { errors }でエラーオブジェクトを取り出している

🔖yupについて

yupは、バリデーションを実施するためのライブラリです。
yupを使用しない場合のバリデーションの設定は、こんな感じになります。register関数の第2引数にバリデーションオブジェクトを渡します。

const { onChange, onBlur, name , ref } = register('password', {
    required: true,
    minLength: 6,
});

第2引数のオブジェクトを変数にすることで、バリデーションロジックをHTML要素から分離することが可能です。このままでも十分なのですが、yupを使うと少しだけバリデーションが設定しやすくなるみたいです。あと、バリデーションの設定がregisterではなく、一階層上のuseFormで設定することになります。少しだけ読みやすいです。
yupResolveryupの関数で、sigininSchemaは自作のバリデーションモジュールです。

import { yupResolver } from '@hookform/resolvers/yup';
import { signinSchema } from '../../validation/validationSchemas';
resolver: yupResolver(signinSchema)

バリデーションの中身は以下のようになります。
コードを見ればわかると思いますが、yupにメソッドをチェーンさせることで、バリデータを作成します。
今回の実装では、バリデータを動的に作成するためにgetPasswordValidatorのようにクロージャにして、バリデータを返す関数を作成しています。

validation/validationSchemas.ts
const emailValidator = yup
    .string()
    .required(requiredMessage)
    .email(emailMessage);

export const getPasswordValidator = (minLength: number) => {
    const passwordRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+$/;
    return yup
        .string()
        .required(requiredMessage)
        .min(minLength, `${minLength}文字以上で入力してください`)
        .matches(passwordRegex, 'パスワードに使用できる文字は、英数字と.!#$%&\'*+/=?^_`{|}~- です');
}

const minLength = 6;
export const signinSchema = yup.object().shape({
    email: emailValidator,
    password: getPasswordValidator(minLength),
});

🔖SubmitHandler

    const onSubmit: SubmitHandler<InputFormData> = async (data) => {
        try {
            await signInWithEmailAndPassword(auth, data.email, data.password);
            router.push('/');
        } catch (e: unknown) {
            const error = e instanceof FirebaseError ? e : e as Error;
            errorHandler(error);
            reset({ email: '', password: '' });
        }
    };

正常処理系と以上処理系に分けて説明します。

onSubmit関数の正常処理系の説明

  • onSubmit関数の型のSubmitHandler<InputFormData>は、react-hook-formで使用するSubmit関数の型です。この関数が引数で受け取るデータの型はInputFormDataという型だという意味です。InputFormDataは、前述のようにformのフィールドがセットされるように実装します。
  • signInWithEmailAndPassword関数を使用してfirebase Authenticationへのログイン処理をおこないます。引数のemailpasswordは、formフィールドの値をセットしている変数名です。事前にfirebase authenticationに登録されていれば、関数を実行すると認証OKとなります。登録がなければ認証エラーがthrowされます。
  • 認証が完了したら/のindex.tsxにジャンプします。

正常系の処理はこれで終了です。

onSubmit関数の異常処理系の説明

  • typescriptで異常処理系を書くときに最初に困るのが、エラーの型です。
    今回の関数では、unknownで受けて、FirebaseError(firebaseのエラーの型)とそれ以外は、Errorに設定しています。eに型をつけないとany型になり型チェックを無効化するらしいのTypeScript的には面白くないようです。いつもリントに怒られます。
  • firebase以外のエラーはError型にしていますが、firebaseの関数とrouter.pushだけなので問題ないでしょう。
  • errorHandlerはカスタムフックが吐き出す関数で、エラー内容をモーダルで表示します。
        <ModalAlert
            isOpen={isOpen}
            modalTitle="Error"
            onClose={onClose}
        >
            {errorMessage}
        </ModalAlert>
    
  • reset関数はreact-hook-formの関数で、認証に失敗したときなどにFormの値をリセットします。

とりあえず、submit関数編はこんな感じです。
次回はreact-hook-form編になります。

Discussion