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)可解决,但会影响项目其他部分的严格模式检查。
问题原因
- React Strict Mode的双重执行:开发环境下,React Strict Mode会主动重复执行
useEffect、状态初始化等函数,用于检测潜在副作用问题。 - 异步操作的竞态条件:第一次
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
相关产品推荐
相关产品推荐

