You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

NextJS 13+Firebase+NextAuth用户重复创建问题求助

解决NextJS 13+Firebase+NextAuth首次登录用户重复创建问题

问题场景

使用NextJS 13、Firebase和NextAuth开发Web应用,通过自定义AuthProvider组件结合createContext管理用户认证,但首次登录时数据库中会生成重复的用户记录。简化后的AuthProvider代码如下:

"use client";
import { createContext, useContext, useEffect, useState } from "react";
import { useSession } from "next-auth/react";
import {
  collection,
  getDocs,
  query,
  where,
  serverTimestamp,
  addDoc,
} from "firebase/firestore";
import { db } from "@/lib/firebaseConfig";

const AuthContext = createContext({});

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const { data: session } = useSession();

  const getUser = async (email) => {
    const q = query(collection(db, "users"), where("email", "==", email));
    const querySnapshot = await getDocs(q);
    const user = [];
    querySnapshot.forEach((doc) => {
      user.push({ ...doc.data(), id: doc.id });
    });
    return user[0];
  };

  const createUser = async (email) => {
    const user = {
      email: session.user.email,
      name: session.user.name,
      image: session.user.image,
      createdAt: serverTimestamp(),
    };
    const docRef = await addDoc(collection(db, "users"), user);
    return { ...user, id: docRef.id };
  };

  useEffect(() => {
    if (session) {
      getUser(session.user.email).then((user) => {
        if (user) {
          setUser(user);
          setLoading(false);
        } else {
          createUser(session.user.email).then((user) => {
            setUser(user);
            setLoading(false);
          });
        }
      });
    } else {
      setLoading(false);
    }
  }, [session]);

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

问题现象

首次登录时createUser函数被调用两次,导致Firestore中生成两条相同的用户记录。关闭React Strict Mode(修改next.config.js中reactStrictMode: false)可解决,但会影响项目其他部分的严格模式检查。

问题原因

  1. React Strict Mode的双重执行:开发环境下,React Strict Mode会主动重复执行useEffect、状态初始化等函数,用于检测潜在副作用问题。
  2. 异步操作的竞态条件:第一次useEffect触发后,getUser查询无结果,开始执行createUser;但Strict Mode触发第二次useEffect时,第一次createUser的Firestore写入还未完成,getUser依然查不到用户,于是再次调用createUser,最终导致重复创建。

修复方案

方案1:添加请求锁,防止并发执行

新增状态变量标记是否正在处理用户初始化逻辑,避免同一时间段内重复执行异步操作:

"use client";
import { createContext, useContext, useEffect, useState } from "react";
import { useSession } from "next-auth/react";
import {
  collection,
  getDocs,
  query,
  where,
  serverTimestamp,
  addDoc,
} from "firebase/firestore";
import { db } from "@/lib/firebaseConfig";

const AuthContext = createContext({});

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  // 添加请求锁状态,标记是否正在处理用户初始化
  const [processing, setProcessing] = useState(false);
  const { data: session } = useSession();

  const getUser = async (email) => {
    const q = query(collection(db, "users"), where("email", "==", email));
    const querySnapshot = await getDocs(q);
    const user = [];
    querySnapshot.forEach((doc) => {
      user.push({ ...doc.data(), id: doc.id });
    });
    return user[0];
  };

  const createUser = async (email) => {
    const user = {
      email: session.user.email,
      name: session.user.name,
      image: session.user.image,
      createdAt: serverTimestamp(),
    };
    const docRef = await addDoc(collection(db, "users"), user);
    return { ...user, id: docRef.id };
  };

  useEffect(() => {
    // 无session或正在处理时直接返回,避免重复执行
    if (!session || processing) return;

    const initUser = async () => {
      setProcessing(true);
      try {
        const existingUser = await getUser(session.user.email);
        if (existingUser) {
          setUser(existingUser);
        } else {
          const newUser = await createUser(session.user.email);
          setUser(newUser);
        }
      } catch (err) {
        console.error("用户初始化失败:", err);
      } finally {
        setLoading(false);
        setProcessing(false);
      }
    };

    initUser();
  }, [session, processing]);

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

方案2:使用Firestore事务保证原子性

利用Firestore的事务机制,确保查询和创建操作的原子性,即使多次触发,也只会创建一次用户:

"use client";
import { createContext, useContext, useEffect, useState } from "react";
import { useSession } from "next-auth/react";
import {
  collection,
  query,
  where,
  serverTimestamp,
  runTransaction,
} from "firebase/firestore";
import { db } from "@/lib/firebaseConfig";

const AuthContext = createContext({});

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const { data: session } = useSession();

  // 原子化的用户初始化逻辑:查询不存在则创建
  const initUserWithTransaction = async (email) => {
    const userCollection = collection(db, "users");
    return await runTransaction(db, async (transaction) => {
      const q = query(userCollection, where("email", "==", email));
      const querySnapshot = await transaction.get(q);
      
      if (!querySnapshot.empty) {
        const doc = querySnapshot.docs[0];
        return { ...doc.data(), id: doc.id };
      } else {
        const newUser = {
          email: session.user.email,
          name: session.user.name,
          image: session.user.image,
          createdAt: serverTimestamp(),
        };
        const docRef = await transaction.add(userCollection, newUser);
        const docSnapshot = await transaction.get(docRef);
        return { ...docSnapshot.data(), id: docSnapshot.id };
      }
    });
  };

  useEffect(() => {
    if (!session) {
      setLoading(false);
      return;
    }

    const initUser = async () => {
      try {
        const user = await initUserWithTransaction(session.user.email);
        setUser(user);
      } catch (err) {
        console.error("用户初始化失败:", err);
      } finally {
        setLoading(false);
      }
    };

    initUser();
  }, [session]);

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

总结

优先选择方案1+方案2的组合:请求锁防止前端重复触发,事务保证后端数据的原子性,既解决Strict Mode下的重复执行问题,也能避免极端情况下的并发创建问题,同时保留Strict Mode的开发检测能力。

内容的提问来源于stack exchange,提问作者Walid Daniel Dib

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 00:47:30