首頁
學習紀錄
遊戲心得影視Life書單案件檔案
Side Projects委託作品與二創互動實驗場
Kurau
百百 BLOG
首頁
學習紀錄
遊戲心得影視Life書單案件檔案
Side Projects委託作品與二創互動實驗場
Kurau

Kurau Blog

「隨心而寫,真真假假,都是我」

一個記錄生活、輸出興趣的個人空間。
遊戲、影視、閱讀、學習……每一段體驗都值得留下文字。

頁面導覽

  • 學習紀錄
  • 遊戲心得
  • 影視Life
  • 書單
  • 委託作品與二創
  • Kurau
  • 合作邀請

找到我

歡迎來 Discord 找我聊天!

“曾經發生的事不可能忘記,只是暫時想不起來而已。”-《神隱少女》

© 2026 Kurau All rights reserved

後端與資料庫

Firebase 驗證 儲存到useContext

By Kurau·2023-04-24·Updated 2026-05-09·4 分鐘閱讀

Firebase Auth + React Context

TL;DR
onAuthStateChanged listener 監聽登入狀態,放進 Context 讓全 app 元件透過 useAuth() 取用。loading state 必加,否則初次載入會閃過「未登入」狀態。Next.js 13+ App Router 寫法略不同(client component + server side cookie session)。
主要參考
LogRocket — Implementing authentication in Next.js with Firebase

完整實作(TypeScript + Pages Router)

1. AuthContext

// contexts/AuthContext.tsx
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import { onAuthStateChanged, User } from 'firebase/auth';
import { auth } from '../lib/firebase';

interface AuthContextType {
  user: User | null;
  loading: boolean;
}

const AuthContext = createContext<AuthContextType>({
  user: null,
  loading: true,
});

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // ⭐ 訂閱 auth 狀態變化(登入 / 登出 / token refresh)
    const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => {
      setUser(firebaseUser);
      setLoading(false);
    });
    return unsubscribe;
  }, []);

  return (
    <AuthContext.Provider value={{ user, loading }}>
      {!loading && children}
    </AuthContext.Provider>
  );
}

export const useAuth = () => useContext(AuthContext);
typescript
{!loading && children} 重要
不檢查 loading 直接渲染 children,SSR 時 user 是 null → 顯示「請登入」→ Firebase 拿到 token 後切換成已登入 → 畫面閃爍。

用 !loading && children 確保 Firebase Auth ready 才渲染,避免閃爍。代價是 initial loading 多 100-500ms,可以加個 skeleton 緩解。

2. 包進 App

// pages/_app.tsx
import { AuthProvider } from '../contexts/AuthContext';

export default function App({ Component, pageProps }: AppProps) {
  return (
    <AuthProvider>
      <Component {...pageProps} />
    </AuthProvider>
  );
}
tsx

3. 在元件中使用

import { useAuth } from '../contexts/AuthContext';

function Profile() {
  const { user, loading } = useAuth();

  if (loading) return <Skeleton />;
  if (!user) return <p>請先登入</p>;

  return (
    <div>
      <img src={user.photoURL} alt="" />
      <p>歡迎, {user.displayName}</p>
      <p>email: {user.email}</p>
    </div>
  );
}
tsx

登入 / 登出函式

// lib/auth-actions.ts
import {
  signInWithEmailAndPassword,
  signInWithPopup,
  GoogleAuthProvider,
  signOut as fbSignOut,
} from 'firebase/auth';
import { auth } from './firebase';

// Email + Password
export async function signInEmail(email: string, password: string) {
  return signInWithEmailAndPassword(auth, email, password);
}

// Google OAuth
const googleProvider = new GoogleAuthProvider();
export async function signInWithGoogle() {
  return signInWithPopup(auth, googleProvider);
}

// 登出
export async function signOut() {
  return fbSignOut(auth);
}
typescript

重點:登入 / 登出後 不需要手動更新 state,onAuthStateChanged 會自動觸發 → context 自動 re-render → 全 app 同步。


保護路由(只有登入才能看)

方案 1:HOC

// hocs/withAuth.tsx
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import { useAuth } from '../contexts/AuthContext';

export function withAuth<P extends object>(Component: React.ComponentType<P>) {
  return function WithAuthComponent(props: P) {
    const { user, loading } = useAuth();
    const router = useRouter();

    useEffect(() => {
      if (!loading && !user) {
        router.push('/login');
      }
    }, [user, loading, router]);

    if (loading || !user) return <Skeleton />;

    return <Component {...props} />;
  };
}
tsx
// 使用
import { withAuth } from '../hocs/withAuth';
function Dashboard() { /* ... */ }
export default withAuth(Dashboard);
tsx

方案 2:Custom Hook

function useRequireAuth(redirectTo = '/login') {
  const { user, loading } = useAuth();
  const router = useRouter();

  useEffect(() => {
    if (!loading && !user) router.push(redirectTo);
  }, [user, loading, router, redirectTo]);

  return { user, loading };
}

// 使用
function Dashboard() {
  const { user, loading } = useRequireAuth();
  if (loading) return <Skeleton />;
  return <div>歡迎 {user!.displayName}</div>;
}
tsx

方案 2 比較現代,沒有 HOC nesting 複雜度。


App Router(Next.js 13+)注意事項

Server Components 不能用 Context,所以 AuthContext 要在 client component:

// app/providers.tsx
'use client';

import { AuthProvider } from '../contexts/AuthContext';

export function Providers({ children }: { children: React.ReactNode }) {
  return <AuthProvider>{children}</AuthProvider>;
}
tsx
// app/layout.tsx
import { Providers } from './providers';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}
tsx

Server Side Auth(進階)

Client-side useAuth() 只在 browser 運作。Server Components 想知道 user 狀態要走 cookie-based session:

// 用 firebase-admin 在 server 驗證 cookie
// app/dashboard/page.tsx
import { cookies } from 'next/headers';
import { adminAuth } from '../lib/firebase-admin';

export default async function Dashboard() {
  const sessionCookie = cookies().get('session')?.value;
  if (!sessionCookie) redirect('/login');

  try {
    const decoded = await adminAuth.verifySessionCookie(sessionCookie);
    return <div>歡迎 {decoded.name}</div>;
  } catch {
    redirect('/login');
  }
}
typescript

這需要設定 Firebase Admin SDK== + 自訂 cookie 流程,比 client-only 複雜。如果 SEO 沒那麼重要,純 client-side 就夠用。


常見地雷

1. 多分頁 sync 問題
同個 user 開多個分頁,在 A 分頁登出 → B 分頁不會自動知道。

解法:onAuthStateChanged 會自動跨分頁同步(因為 Firebase 用 IndexedDB / localStorage 存 token,被改動會觸發其他分頁的 listener)。

2. SSR Hydration mismatch
Server 渲染時 user 是 null,client 立即知道是登入 → React 抱怨 hydration mismatch。

解法:確保 loading state 處理。或者用 useState + useEffect 延遲渲染,first paint 不依賴 user 狀態。

3. token 過期
Firebase ID token 每小時自動 refresh,通常無感。但如果你用 token 呼叫自家後端,要在 server 驗證 + handle 過期。
// 在前端取最新 token
const token = await user.getIdToken(true);  // true = force refresh
fetch('/api/protected', {
  headers: { Authorization: `Bearer ${token}` },
});
typescript

Auth 流程實際範例

// app/login/page.tsx
'use client';
import { useState } from 'react';
import { signInEmail, signInWithGoogle } from '@/lib/auth-actions';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/contexts/AuthContext';
import { useEffect } from 'react';

export default function LoginPage() {
  const { user } = useAuth();
  const router = useRouter();
  const [email, setEmail] = useState('');
  const [pwd, setPwd] = useState('');
  const [err, setErr] = useState('');

  // 已登入直接導走
  useEffect(() => {
    if (user) router.push('/dashboard');
  }, [user, router]);

  const handleEmailLogin = async () => {
    try {
      await signInEmail(email, pwd);
      // ⭐ 不用手動 router.push,上面 useEffect 會處理
    } catch (e: any) {
      setErr(e.message);
    }
  };

  return (
    <form onSubmit={(e) => { e.preventDefault(); handleEmailLogin(); }}>
      <input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="email" />
      <input type="password" value={pwd} onChange={(e) => setPwd(e.target.value)} placeholder="password" />
      <button type="submit">Login</button>
      <button type="button" onClick={signInWithGoogle}>Google</button>
      {err && <p style={{ color: 'red' }}>{err}</p>}
    </form>
  );
}
tsx

目錄

    ◆ 相關文章

    • Firebase 前端API 是安全的

      2026-05-09
    • CORS firebase

      2026-05-09
    • Firebase 資料庫如何建立 與 上傳

      2026-05-09
    • 一開始就獲取Firebase storage初始值

      2026-05-09
    ← 上一篇Git教學下一篇 →Firebase 前端API 是安全的

    ◆ 關於作者

    Kurau

    個人寫作 / 創作的 SoT,記錄遊戲、影視、學習與生活。

    更多 Kurau 的文章