Nextjs+ChakraUi+FirebaseAuth+react-hook-formで作るログイン画面(その1:ロジック編)
概要
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.tsexport const firebaseApp = initializeApp(firebaseConfig)
firebaseのプロジェクトを作成したら生成されるJSONオブジェクトです。(apiKeyとかappIdとかが入っているやつ) - useRouterは、Next.jsのカスタムフックで、出力のrouterで、ページ遷移を行います。
- useErrorHandlerは、独自のエラー用カスタムフックでポップアップ画面(今回はModalAlertコンポーネント)などの制御に使用します。
🔖フォーム初期値設定部
const formDefaultValues: InputFormData = {
email: '',
password: '',
};
この部分は、フォームの初期値に型を明示しているだけです。useFormのPropsに直接記述しても問題ないです。
🔖react-hook-form実行部
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管理に役立つ関数などを出力します。
useFormの入力値
-
mode:
バリデーションのタイミングを設定する属性です。allはonBlurとonChangeの両方で実行されるようです。その他の情報は、公式を参照して下さい。 -
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で設定することになります。少しだけ読みやすいです。
yupResolverはyupの関数で、sigininSchemaは自作のバリデーションモジュールです。
import { yupResolver } from '@hookform/resolvers/yup';
import { signinSchema } from '../../validation/validationSchemas';
resolver: yupResolver(signinSchema)
バリデーションの中身は以下のようになります。
コードを見ればわかると思いますが、yupにメソッドをチェーンさせることで、バリデータを作成します。
今回の実装では、バリデータを動的に作成するためにgetPasswordValidatorのようにクロージャにして、バリデータを返す関数を作成しています。
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へのログイン処理をおこないます。引数のemailとpasswordは、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