🐣

React Hook Form、Zodで職務経歴書登録フォーム開発(準備編)

2023/10/02に公開

モチベーション

今回使用したライブラリーのバージョン

  • Next.js: 13.4.19
    • 最新は13.5.3でしたが後述のエラーが発生したためダウングレード
  • React: 18.2.0
  • React Hook Form: 次回導入
  • Zod: 次回導入
  • MUI: 5.14.11
  • TailwindCSS: 3.3.3
  • Recoil: 0.7.7
  • TypeScript: 5.2.2
  • Firebase: 10.4.0

今回行ったことまとめ

  • Next.jsにMUIのカスタムテーマを適用
  • サインイン機能開発
  • FirebaseにNext.jsアプリをホスティング
  • GitHub、Firebase連携

React Hook Form、Zodはまだ使っていません!(タイトル詐欺)

Next.jsにMUIのカスタムテーマを適用

Using Material UI with a custom theme に従い Theme Registry を作成し、 RootLayout の中で利用するようにしました。

カラーパレットにカスタムカラーを定義

今回、一部のコンポーネントだけ他と色合いを変更しようと思い、Custom colors を参考に以下のようにカスタムカラーを定義してみました。

src\styles\theme.ts
import { createTheme } from "@mui/material";

// PaletteとPaletteOptionsにキーを追加
declare module "@mui/material/styles" {
  interface Palette {
    blue: Palette["primary"];
  }

  interface PaletteOptions {
    blue?: PaletteOptions["primary"];
  }
}

export const theme = createTheme({
  palette: {
    primary: {
      main: "#ff0000",
    },
    secondary: {
      main: "#ffffff",
    },
    // 新しい色を定義
    blue: {
      main: "#4285f4",
      light: "#8ab4f8",
      dark: "#0d47a1",
      contrastText: "#FFFFFF",
    },
  },
  typography: {
    fontFamily:
      "Noto Sans JP, Yu Gothic, Meiryo, Hiragino Kaku Gothic Pro, sans-serif",
    button: {
      textTransform: "none",
    },
  },
});

サインイン機能開発

基本的には Vite + React + FirebaseでGoogle認証機能を作ったときのメモ と同じように開発しましたが、いくつか注意すべき点があったので以下に書きます。

ViteとNext.jsとでは環境変数の命名ルールや読み込み方が異なる

Viteでは以下のようにして環境変数を読み込んでいました。

  • 環境変数名の初めには VITE_ を付与する
  • import.meta.env.{環境変数名} でその環境変数の値を利用する
const firebaseConfig = {
  apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
  authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
  projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
  storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
  appId: import.meta.env.VITE_FIREBASE_APP_ID,
  mesurementId: import.meta.env.VITE_FIREBASE_MEASUREMENT_ID,
};

一方、Next.jsでは以下のようにする必要があります。

  • クライアントで環境変数を利用する場合は環境変数名の初めには NEXT_PUBLIC_ を付与する
  • process.env.{環境変数名} でその環境変数名の値を利用する
const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
  mesurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
};

当初 NEXT_PUBLIC_ を付与しておらず「環境変数取れないなぁ。なんでだろう?」と若干悩んでいましたが、 Next.jsにおける環境変数 (env) の取り扱い を参照し解決しました。

useRouterは "next/navigation" のものを使うべし

ページ遷移する際に import { useRouter } from "next/navigation" ではなく import { useRouter } from "next/router" を利用していて NextRouter was not mounted Next.JS というエラーが出ていました。よく見ずにVSCodeの補完機能に頼っているとこうなりそうです(なった)。

App Routerの場合、"next/navigation" を使う必要がある旨は Nnxt 13 appディレクトリでuseRouterを使用するとエラーが表示される などにも書かれていました。

Hydratingエラー

当初、Recoilで管理する状態(ログインユーザー)の初期値を以下のように設定していました。ローカルストレージに値があればそれを利用するようにしています。

export const userAtom = atom({
  key: "userAtom",
  default: {
    userId: localStorage.getItem("userId") || null,
    userName: localStorage.getItem("userName") || null,
  } as LoginUser,
});

ただ、これだとサーバーサイドで生成するHTMLとクライアントサイドで生成するHTMLに差が生まれてしまいます。サーバーサイドではローカルストレージは利用できないためです。これにより以下のエラーが発生しました。

react-dom.development.js:15468  Uncaught Error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.

これを回避するため、初期値はnullとしてこの状態を利用するコンポーネントでuseEffectを利用しローカルストレージから値を取得するようにしました。Bingにそう勧められたのでやってみましたが、なぜこれでいいのかはイマイチわかっていないのでApp Routerを真面目に勉強します・・・。

useEffect(() => {
    let _userId = localStorage.getItem("userId");
    let _userName = localStorage.getItem("userName");
    if (loginUser.userId) {
      _userId = loginUser.userId;
    }
    if (loginUser.userName) {
      _userName = loginUser.userName;
    }
    setLoginUser((prev: LoginUser) => {
      return {
        ...prev,
        userId: _userId || null,
        userName: _userName || null,
      };
    });
  }, [loginUser.userId, loginUser.userName, setLoginUser]);

FirebaseにNext.jsアプリをホスティング

Next.js を統合する に書かれていますが、フレームワーク対応の Hosting は早期公開プレビュー版です とのことです(だから後述のエラーも発生したのかなと想像)。

ウェブフレームワークのプレビューを有効にする

手順は Next.js を統合する の通りでまずはウェブフレームワークのプレビューを有効にします。

firebase experiments:enable webframeworks

初期化コマンドを実行

ここはいつも通り firebase initfirebase init hosting を実行するだけなので詳細割愛します。

デプロイ -> エラー発生

次に firebase deploy コマンドを実行したところ以下のエラーが発生しました。

TypeError: Cannot destructure property 'customConfig' of '(intermediate value)(intermediate value)(intermediate value)' as it is null.

https://github.com/firebase/firebase-tools/issues/6382 に書かれていますが、どうもNext.jsが13.5以上だとエラーになるようなので、13.4.19にダウングレードしエラー解消しました。

GitHub、Firebase連携

これも基本は Firebase、GitHub Actions連携 と同じですが、1つだけ注意点がありました。なりゆきだとGitHub ActionsでFirebaseにデプロイする際に以下のエラーが発生します。

Error: Cannot deploy a web framework from source because the experiment webframeworks is not enabled. To enable add a FIREBASE_CLI_EXPERIMENTS environment variable to .github/workflows/firebase-hosting-merge.yml, like so: 
  
  - uses: FirebaseExtended/action-hosting-deploy@v0
    with:
      ...
    env:
      FIREBASE_CLI_EXPERIMENTS: webframeworks

エラーメッセージ通り環境変数を追加するとエラー解消します。以下修正後のワークフローです。

firebase-hosting-merge.yml
# This file was auto-generated by the Firebase CLI
# https://github.com/firebase/firebase-tools

name: Deploy to Firebase Hosting on merge
"on":
  push:
    branches:
      - main
jobs:
  build_and_deploy:
    runs-on: ubuntu-latest
    env: # ここを追加
      NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.FIREBASE_API_KEY }}
      NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ secrets.FIREBASE_AUTH_DOMAIN }}
      NEXT_PUBLIC_FIREBASE_PROJECT_ID: ${{ secrets.FIREBASE_PROJECT_ID }}
      NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: ${{ secrets.FIREBASE_STORAGE_BUCKET }}
      NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.FIREBASE_MESSAGING_SENDER_ID }}
      NEXT_PUBLIC_FIREBASE_APP_ID: ${{ secrets.FIREBASE_APP_ID }}
      NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.FIREBASE_MEASUREMENT_ID }}
      FIREBASE_CLI_EXPERIMENTS: webframeworks
    steps:
      - uses: actions/checkout@v3
      - run: npm ci && npm run build
      - uses: FirebaseExtended/action-hosting-deploy@v0
        with:
          repoToken: "${{ secrets.GITHUB_TOKEN }}"
          firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_NEXTJS_A609C }}"
          channelId: live
          projectId: nextjs-a609c

コード

https://github.com/shoji9x9/firebase-nextjs

アプリケーション

https://nextjs-a609c.web.app/

次回予定

ここまででお腹いっぱいなので、タイトル回収(React Hook Form、Zodの利用)は次回に持ち越します!

Discussion