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} />
</>
);
}
この構成により、全ページでuser
をprops
経由で受け取れるようになります
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-hooks
の useAuthState
を使ってログイン状態を取得します
user
: ログイン中のユーザーオブジェクト(未ログインなら null
)
loading
: 認証情報を取得中であるかどうか
const hasTriedLogin = useRef(false);
useState
は、UIに関係する状態を管理するためのフックで、値が更新されるとコンポーネントが再レンダリングされます
一方、useRef
は再レンダリングを引き起こさずに値を保持できるため、ユーザーに見せる必要のないフラグや一時的なデータ、またはDOM要素への参照を扱う場面に適しています
hasTriedLogin
は、ログイン処理の二重実行(多重ポップアップや無限ループ)を防ぐためのフラグで、再レンダリング不要な情報のためuseRef
で保持しています
useEffect(() => {
if (loading || hasTriedLogin.current) return;
Firebaseの認証状態取得が完了しているかの判定で、ログイン処理を試みていなければ処理を継続します
.current
でhasTriedLogin
の値にアクセスしています
if (user) {
if (redirect === "/meishi") {
router.replace(`/meishi/${user.uid}`);
} else {
router.replace(redirect);
}
return;
}
既にログイン済みの場合は、ログイン処理を起動せずにリダイレクトだけ行います
/meishi へのリダイレクトでは、uid(user id)
をURLに付加することで、ユーザー別のプロフィールに遷移することができます
router.replace
はrouter.push
とは異なり、現在のページのURLを履歴に残さずに指定されたパス(redirect)に遷移します
hasTriedLogin.current = true;
ログイン処理の二重実行を防止するため hasTriedLogin.current
をtrue
に更新しています
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の副作用フックで、loading
やuser
が変化するたびに、この関数が実行されます
Discussion