如何在Context的authenticatedUser更新后触发组件重渲染?
场景与问题
我在UserAuthContext中向子组件提供用户相关数据与方法:
<userAuthContext.Provider value={{ user, authenticatedUser, logIn, signUp, logOut }}> {children} </userAuthContext.Provider>
原本获取authenticatedUser的逻辑是创建一个User实例,在useEffect中从Firebase获取数据后调用实例的updateUserData方法更新内部状态:
const authenticatedUser = new User(); const { updateUserData } = authenticatedUser; useEffect(() => { const getUserData = async () => { const docSnap = await getDoc(doc(db, "registeredUsers", user.uid)); if (docSnap.exists()) { const userData = docSnap.data(); const mappedUserData = { userUsername: "George", userEmail: userData.userEmail, userDateOfBirth: userData.userDateOfBirth, userUid: userData.userUid, userVerified: userData.userVerified, userIsSeller: userData.userIsSeller, userRewardLevel: userData.userRewardLevel, }; updateUserData(mappedUserData); } else { console.log("No such document!"); } }; if (user && user.uid) { getUserData(); } }, [user]);
组件中通过useUserAuth获取数据后使用:
const { user, authenticatedUser, logOut } = useUserAuth(); <h1>{authenticatedUser && authenticatedUser.userState && authenticatedUser.userState.userUsername}</h1>
此时authenticatedUser的数据确实存在,但组件完全渲染完成后数据才可用,尝试用useEffect包裹逻辑也无法触发重渲染。
后来我通过添加一个临时状态test并更新它,实现了触发重渲染,但这显然不是最优解:
const authenticatedUserr = new User(); const { updateUserData } = authenticatedUserr; const [test, setTest] = useState({}); useEffect(() => { const getUserData = async () => { const docSnap = await getDoc(doc(db, "registeredUsers", user.uid)); if (docSnap.exists()) { const userData = docSnap.data(); const mappedUserData = { userUsername: "George", userEmail: userData.userEmail, userDateOfBirth: userData.userDateOfBirth, userUid: userData.userUid, userVerified: userData.userVerified, userIsSeller: userData.userIsSeller, userRewardLevel: userData.userRewardLevel, }; updateUserData(mappedUserData); setTest(authenticatedUserr) } else { console.log("No such document!"); } }; if (user && user.uid) { getUserData(); } }, [user]);
核心原因
React组件重渲染的触发条件是自身状态更新或父组件传递的props变化。在Context中,只有当Provider的value发生引用变化时,消费Context的组件才会重渲染。
之前的authenticatedUser是一个自定义类实例,调用updateUserData只是修改了实例内部的状态,但实例本身的引用没有改变,因此Context的value没有变化,组件不会触发重渲染。临时添加的test状态通过setTest更新后,触发了Context所在组件的重渲染,进而让子组件感知到变化,但这是绕弯子的做法。
正确解决方案
直接用React的useState来管理authenticatedUser,当数据更新时通过setAuthenticatedUser修改状态,这样会自动触发Context的更新,进而让所有消费组件重渲染。
修改后的完整Context代码:
import { createContext, useContext, useState, useEffect } from 'react'; import { getDoc, doc } from 'firebase/firestore'; import { db } from './firebaseConfig'; // 假设你的Firebase配置文件路径 const UserAuthContext = createContext(); export function UserAuthProvider({ children }) { const [user, setUser] = useState(null); // 假设这是Firebase Auth的用户状态,比如从onAuthStateChanged获取 // 用useState管理authenticatedUser,初始值设为null const [authenticatedUser, setAuthenticatedUser] = useState(null); // 示例logIn、signUp、logOut方法(根据实际业务逻辑实现) const logIn = async (email, password) => { // 实现登录逻辑,成功后更新user状态 }; const signUp = async (email, password, username) => { // 实现注册逻辑,成功后更新user状态并写入Firestore }; const logOut = async () => { // 实现登出逻辑,重置user和authenticatedUser状态 setUser(null); setAuthenticatedUser(null); }; useEffect(() => { const getUserData = async () => { // 当user不存在或无uid时直接返回 if (!user?.uid) { setAuthenticatedUser(null); return; } try { const docSnap = await getDoc(doc(db, "registeredUsers", user.uid)); if (docSnap.exists()) { const userData = docSnap.data(); // 直接构造用户数据对象,不需要自定义User类 const mappedUserData = { userUsername: "George", userEmail: userData.userEmail, userDateOfBirth: userData.userDateOfBirth, userUid: userData.userUid, userVerified: userData.userVerified, userIsSeller: userData.userIsSeller, userRewardLevel: userData.userRewardLevel, }; // 更新authenticatedUser状态 setAuthenticatedUser(mappedUserData); } else { console.log("No such document!"); setAuthenticatedUser(null); } } catch (error) { console.error("Error getting user data:", error); setAuthenticatedUser(null); } }; getUserData(); }, [user]); return ( <UserAuthContext.Provider value={{ user, authenticatedUser, logIn, signUp, logOut }}> {children} </UserAuthContext.Provider> ); } // 自定义Hook用于获取Context export const useUserAuth = () => useContext(UserAuthContext);
组件中使用时可以简化为:
import { useUserAuth } from './UserAuthContext'; function Profile() { const { authenticatedUser, logOut } = useUserAuth(); return ( <div> <h1>{authenticatedUser?.userUsername}</h1> <button onClick={logOut}>登出</button> </div> ); } export default Profile;
这样修改后,当authenticatedUser通过setAuthenticatedUser更新时,Context的value会发生变化,所有使用useUserAuth的组件都会自动触发重渲染,直接拿到最新的用户数据。
内容的提问来源于stack exchange,提问作者JohnDole

