💡

Astro+React Hook Form+FirebaseでZodを効かせつつユーザー登録を行う

2024/01/04に公開

2023年の11月頃からReactとAstroを本格的に扱うようになりました。
特にAstroはWebフレームワークとしてのコンセプトから気に入り、情報を積極的に仕入れています。
さて、今回はタイトルの通りですがAstroでReact Hook FormとZodを使ってFirebase Authenticationにユーザー登録をしてみようという内容です。

本記事では、Firebase Authenticationのブロッキング関数に関しては割愛しておりますので、ご了承下さい。
それでは早速いってみましょう。

実行環境・バージョン

astro@4.0.9
react-hook-form@7.49.2
zod@3.22.4
firebase-admin@12.0.0
firebase@10.7.1
@astrojs/node@7.0.4

1.AstroをFirebaseと連携させる準備

https://docs.astro.build/ja/guides/backend/google-firebase/

記事執筆時点で日本語訳されていませんが、公式ドキュメントに手順が載せられています。
このドキュメント通りにまずは進めます。

2.AstroをSSRとして使えるようにする

https://docs.astro.build/ja/guides/server-side-rendering/#_top

astro.config.mjsファイルでSSR対応させる記述を行います。

import { defineConfig } from 'astro/config';
import nodejs from '@astrojs/node';

export default defineConfig({
  adapter: nodejs({
    mode: 'middleware'
  }),
  output: 'hybrid',
});

outputをhybridにする方法とserverにする方法がありますが、今回はhybridを採用します。
※serverとhybridの違い
output: 'server'

export const prerender = true
この除外記述がないページは全てSSRで動きます。

output: 'hybrid'

export const prerender = false
この除外記述があるページをSSRで動かします。

3.登録ページをコーディング

---
import Layout from "../layouts/Layout.astro";
import Form from "../components/RegisterForm";

export const prerender = false;
---

<Layout title="ユーザー新規登録" description="ユーザー新規登録画面">
  <Form client:only="react"/>
</Layout>

export const prerender = false;
これでSSRで動かすと指定しつつ、後述するFormコンポーネントにはclient:only="react"を指定しておきます。

4.登録用のファイルを用意

pages/api/auth/register.ts

import type { APIRoute } from "astro";
import { getAuth } from "firebase-admin/auth";
import { app } from "../../../firebase/server";

export const POST: APIRoute = async ({ request, redirect }) => {
  const auth = getAuth(app);

  /* Get form data */
  const formData = await request.formData();
  const email = formData.get("email")?.toString();
  const password = formData.get("password")?.toString();
  const name = formData.get("name")?.toString();

  if (!email || !password || !name) {
    return new Response(
      "データが見つかりません",
      { status: 400 }
    );
  }

  /* Create user */
  try {
    await auth.createUser({
      email,
      password,
      displayName: name,
    });
  } catch (error: any) {
    return new Response(
      "ユーザーの作成に失敗しました",
      { status: 400 }
    );
  }
  return redirect("/signin");
};

公式ドキュメント内にも記載されていますが、紹介しておきたいと思います。
このファイルにフォームの内容をfetchでPOST送信することになります。

5.フォームコンポーネントをコーディング

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

const PassCode = new RegExp("^(?=.*[A-Z])(?=.*[.?/-])[a-zA-Z0-9.?/-]{8,24}$");

const schema = z.object ({
  name: z.string().min(1,{
    message: '1文字以上入力して下さい'
  }),
  email: z.string().email({
    message: 'メールアドレスの形式で入力して下さい'
  }).min(1,{
    message: '1文字以上入力して下さい'
  }),
  password: z.string().regex(PassCode,{
    message: 'アルファベット大文字とピリオド(.)、スラッシュ(/)、クエスチョンマーク(?)、ハイフン(-)のどれかを含めた、8-24文字で設定して下さい'
  })
});

export default function Form() {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: zodResolver(schema),
  });

  const onSubmit = async (data: any, e:SubmitEvent) => {
    e.preventDefault();
    const form = new FormData();
    form.append('name', data.name);
    form.append('email', data.email);
    form.append('password', data.password);

    await fetch('/api/auth/register', {
      method: "POST",
      body: form
    })
    .then(response => {
      if (!response.ok) {
        console.error('サーバーエラー');
      }
      // ここに成功時の処理を記述
      alert('登録が正常に完了しました');
      window.location.href = '/signin/';
    })
    .catch(error => {
      console.error('通信に失敗しました', error);
    });
  };

  return (
    <div className="Form">
      <form onSubmit={handleSubmit(onSubmit)}>
        <div className="flex flex-col gap-4">
          <div className="flex gap-3">
            <label htmlFor="name" >ユーザー名</label>
            <input {...register('name')} id="name" className="border"/>
          </div>
          <p>{errors.name?.message}</p>
          <div className="flex gap-3">
            <label htmlFor="email" >メールアドレス</label>
            <input {...register('email')} id="email" className="border"/>
          </div>
          <p>{errors.email?.message}</p>
          <div className="flex gap-3">
            <label htmlFor="password">パスワード</label>
            <input {...register('password')} type="password" id="password" className="border"/>
          </div>
          <p>{errors.password?.message}</p>
          <button type="submit">新規登録</button>
          </div>
      </form>
    </div>
  );
}

tailwindCSSを採用していたので、HTMLのクラスはその名残ですね…
ポイントですがfetchでPOST送信するデータをFormDataオブジェクトにセットして送るようにすればOKです。

フォームに間違った値を入力して新規登録の部分をクリックすると…

このような感じでエラーが入力欄の付近に表示されるという感じで機能してくれます。
正しく入力すると…

アラートが表示されて、サインイン画面にリダイレクトされる…といった感じです。

Firebaseの方も確認してみますと…

こんな感じで、無事に登録ができているのが確認できました。

最後に

Firebase側のブロッキング関数の設定などもセキュリティ上で必要になってきますが、フロント側でもバリデーションが効かせられてユーザビリティの向上・セキュリティ施策の一環としても良い方法なのかなと思いました。

Astro・React・Firebaseの連携で様々な開発ができそうだと感じたので、これからも構築のレベルを上げていきたいと思います。

以上、お役に立ちましたら幸いです。

Discussion