💡

Firebase Auth × Next.js 認証状態の管理とページ遷移時の工夫

に公開

はじめに

Firebase AuthenticationをNext.jsアプリに組み込む際、どのように実装したのかをまとめようと思います(https://github.com/YamaU10622/igo-meishi)

ログイン状態の管理方法、ログイン処理、リダイレクト設計についての実装の説明をしています

また、ページ遷移時のログイン状態管理でつまずいた部分があるので、それについてもまとめました

前提

テーマとするアプリは以下のような構成です(一部省略)
nextのバージョンは15.3.0です

igo-meishi:.

├─components
│      Footer.jsx
│      Header.jsx
│      MeishiForm.jsx
│
├─pages
│  │  create.js
│  │  index.js
│  │  login.js
│  │  _app.js
│  │
│  ├─edit
│  │      [normalizedName].js
│  │
│  └─meishi
│          [normalizedName].js
│
├─public
└─styles

ログイン状態の管理と表示

Googleアカウントのログイン状態をアプリ全体で管理するため、pages/_app.js で Firebaseライブラリの onAuthStateChanged を使い、ユーザー情報を監視します。

_app.js

import { useEffect, useState } from 'react';
import { onAuthStateChanged } from 'firebase/auth';
import { auth } from '../lib/firebase';
import Header from '../components/Header';

export default function App({ Component, pageProps }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => {
      setUser(firebaseUser);
    });
    return () => unsubscribe();
  }, []);

  return (
    <>
      <Header user={user} />
      <Component {...pageProps} user={user} />
    </>
  );
}

この構成により、全ページでuserprops経由で受け取れるようになります
Firebase Authenticationはセッションベースのログイン状態を自動で保持してくれます

ログイン処理とリダイレクト設計

login.js

import { useEffect, useRef } from 'react';
import { useRouter } from 'next/router';
import { signInWithPopup, GoogleAuthProvider } from 'firebase/auth';
import { auth } from '../lib/firebase';
import { useAuthState } from "react-firebase-hooks/auth";

export default function LoginPage() {
  const router = useRouter();
  const { redirect = "/" } = router.query;
  const [user, loading] = useAuthState(auth);
  const hasTriedLogin = useRef(false);

  useEffect(() => {
    if (loading || hasTriedLogin.current) return;

    if (user) {
      if (redirect === "/meishi") {
        router.replace(`/meishi/${user.uid}`);
      } else {
        router.replace(redirect);
      }
      return;
    }

    hasTriedLogin.current = true;

    const provider = new GoogleAuthProvider();
    signInWithPopup(auth, provider)
      .then((result) => {
        const uid = result.user?.uid;
        if (redirect === "/meishi") {
          router.replace(`/meishi/${uid}`);
        } else {
          router.replace(redirect);
        }
      })
      .catch((error) => {
        console.error("ログインエラー:", error);
        if (error.code !== "auth/cancelled-popup-request") {
          alert("ログインに失敗しました");
        }
        router.replace("/");
      });
  }, [user, loading, redirect, router]);

  return null;
}

解説

const { redirect = "/" } = router.query;

redirect はクエリパラメータから取得され、遷移後のページを示します
指定されていなければデフォルトはルート"/"

const [user, loading] = useAuthState(auth);

react-firebase-hooksuseAuthState を使ってログイン状態を取得します
user: ログイン中のユーザーオブジェクト(未ログインなら null
loading: 認証情報を取得中であるかどうか

const hasTriedLogin = useRef(false);

useStateは、UIに関係する状態を管理するためのフックで、値が更新されるとコンポーネントが再レンダリングされます
一方、useRef は再レンダリングを引き起こさずに値を保持できるため、ユーザーに見せる必要のないフラグや一時的なデータ、またはDOM要素への参照を扱う場面に適しています
hasTriedLoginは、ログイン処理の二重実行(多重ポップアップや無限ループ)を防ぐためのフラグで、再レンダリング不要な情報のためuseRefで保持しています

useEffect(() => {
    if (loading || hasTriedLogin.current) return;

Firebaseの認証状態取得が完了しているかの判定で、ログイン処理を試みていなければ処理を継続します
.currenthasTriedLoginの値にアクセスしています

if (user) {
  if (redirect === "/meishi") {
    router.replace(`/meishi/${user.uid}`);
  } else {
    router.replace(redirect);
  }
  return;
}

既にログイン済みの場合は、ログイン処理を起動せずにリダイレクトだけ行います
/meishi へのリダイレクトでは、uid(user id)をURLに付加することで、ユーザー別のプロフィールに遷移することができます
router.replacerouter.pushとは異なり、現在のページのURLを履歴に残さずに指定されたパス(redirect)に遷移します

hasTriedLogin.current = true;

ログイン処理の二重実行を防止するため hasTriedLogin.currenttrueに更新しています
result.user?.uid?.はオプショナルチェーン演算子というものです
このように記述することで、userが存在していない場合でもundefinedを返し、アプリがクラッシュするのを防ぎます

createページでの問題と解決

問題:ログイン済みでも /login にリダイレクトされる

useEffect(() => {
  if (!user) {
    router.replace("/login?redirect=/create");
  }
}, []);

create.js ページでuseEffectを使ってuserをチェックし、未ログインなら /login に遷移する処理を最初記述しました
しかしページ初期読み込み時、まだuserが取得されていない段階でnull判定されてしまい、誤ってログインページに飛ばされる現象が起きました

対応:認証状態の修正

const [user, loading] = useAuthState(auth);
const router = useRouter();

useEffect(() => {
    if (!loading && !user) {
      router.replace("/login?redirect=/create");
    }
    }, [loading, user]);
    
    if (loading) {
    return null;
    }
    
    if (!user) {
    return null; // リダイレクト中
}

このように、loading(認証情報を取得中かどうか)を追加することにより、未ログインの状態と、認証情報取得中の状態を区別することで解決しました
ちなみに、useEffect(() => { ... }, [loading, user]);はReactの副作用フックで、loadinguserが変化するたびに、この関数が実行されます

Discussion