我正在开发一个 React Native 应用程序,在用户通过身份验证之前让登录页面停止闪烁的唯一方法是添加一个 setTimeout,如下所示:
export default function App() { const [IsReady, setIsReady] = useState(false); const LoadFonts = async () => { await useFonts(); }; if (!IsReady) { return ( <AppLoading startAsync={LoadFonts} onFinish={() => setTimeout(() => { setIsReady(true); }, 1000) } onError={(error) => { console.log(error); }} /> ); } return <Providers />;
这是不好的做法吗?有什么更好的方法可以解决这个问题?我必须这样做的原因是,在我的 Routes.js 文件中,我会检查用户是否经过身份验证,如果没有,他们就会获得登录堆栈导航。如果是,他们就会获得主页。
路线.js
export default function Routes() { const { user, setUser, setFirestoreUserData } = useContext(AuthUserContext); useEffect(() => { const unsubscribeAuth = auth.onAuthStateChanged(async (authUser) => { await (authUser ? setUser(authUser) : setUser(null)); }); return unsubscribeAuth; }, []); useEffect(() => { const fetchData = async () => { const getUserObject = await getUserFromFirestore(user.uid); setFirestoreUserData(getUserObject); }; if (user) { fetchData(); } }, [user]); return ( <NavigationContainer theme={navigationTheme}> {user ? <TabStack /> : <AuthStack />} </NavigationContainer> ); }
如果有人对如何重构这一点有任何建议,我将不胜感激!谢谢。
更新:此答案已更新为使用较新的 Modular Firebase SDK (v9+)。有关旧版 Namespaced Firebase SDK (v8 或更早版本) 的代码,请参阅旧版本。
如果您不确定使用哪个,请使用 Modular Firebase SDK。旧版本不会有任何更新。
您遇到的闪光是由您的Routes组件引起的。
Routes
这是因为当用户首次加载您的网站时,他们的身份验证状态为待定。在 SDK 表示用户已正确登录之前,它必须先致电 Firebase 身份验证服务器以检查该用户会话是否有效。如果您firebase.auth().currentUser在此过程中调用,它将返回null。
firebase.auth().currentUser
null
因此,因为您的Routes组件包含用户来自的以下几行firebase.auth().currentUser:
return ( <NavigationContainer theme={navigationTheme}> {user ? <TabStack /> : <AuthStack />} </NavigationContainer> );
您的页面会在 时短暂渲染,AuthStack然后在确认用户会话后重新渲染。user``null``TabStack
AuthStack
user``null``TabStack
无论您使用什么值setTimeout,这种情况都会发生,App因为它与该值无关,而是在您的Providers组件下。
setTimeout
App
Providers
要纠正这个问题,您必须在确认身份验证会话时null从组件返回。Routes
由于我不熟悉您的AuthUserContext实现,因此我将根据这个(它处理用户的状态和他们的主要用户数据)来回答。
AuthUserContext
// ./FirebaseAuthUserContext.jsx import { createContext, useContext } from "react"; import { app } from './firebase.js' // should contain `export const app = initializeApp(...)` import { getAuth, onAuthStateChanged } from 'firebase/auth'; import { getFirestore, doc, onSnapshot } from 'firebase/firestore'; export const FirebaseAuthUserContext = createContext({ // the current user's data data: undefined, // more information about the current user's data dataInfo: { status: "loading" }, // the status of fetching the current user's data dataStatus: "loading", // the status of checking the user's auth session initializing: true, // the current user object user: undefined }); export const useAuth = () => useContext(FirebaseAuthUserContext); export function FirebaseAuthUserProvider({children}) { // If authentication state has been determined already, // use the current user object. Otherwise, fall back to `undefined`. const [user, setUser] = useState(() => getAuth(app).currentUser || undefined); // If initial `user` value is `undefined`, we need to initialize. const [initializing, setInitializing] = useState(user === undefined); // Prep a space to hold their user data // - data?: user data object, as applicable // - error?: error related to the user's data, as applicable // - ref?: reference to user data's location, as applicable // - status: status of the user data const [userDataInfo, setUserDataInfo] = useState({ status: "loading" }); useEffect(() => onAuthStateChanged(getAuth(app), (user) => { setUser(user); // user is User | null if (initializing) setInitializing(false); }), []); useEffect(() => { if (initializing) return; // do nothing, still loading auth state. if (user === null) { setUserDataInfo({ status: 'signed-out', data: null }); return; } const userDataDocRef = doc(getFirestore(app), "users", user.uid); return onSnapshot( // <- returns an unsubscribe callback userDataDocRef, { next: (snapshot) => { if (snapshot.exists) { setUserDataInfo({ status: "loaded", get data() { return snapshot.data() }, ref: userDataDocRef }); } else { setUserDataInfo({ status: "not-found", data: null, ref: userDataDocRef }) } }, error: (error) => setUserDataInfo({ status: 'error', data: null, error }) } ); }, [user]); // you can rename these as desired: return ( <FirebaseAuthUserContext.Provider value={{ get data() { return userDataInfo.data }, // for convenience dataInfo: userDataInfo, dataStatus: userDataInfo.status, // for convenience initializing, user }}> {children} </FirebaseAuthUserContext.Provider> ); }
注意:您可以将用户数据从此分离FirebaseAuthUserContext到另一个上下文中FirebaseAuthUserDataContext,但是通常情况下,您无论如何都需要两者,因此您可以将它们放在一起。
FirebaseAuthUserContext
FirebaseAuthUserDataContext
使用上述Context对象,您可以将Routes组件更新为:
Context
import useAuth from './FirebaseAuthUserContext.jsx'; export default function Routes() { const userInfo = useAuth(); if (userInfo.initializing) return null; // hide while loading return ( <NavigationContainer theme={navigationTheme}> {userInfo.user ? <TabStack /> : <AuthStack />} </NavigationContainer> ); }
下面是使用该Context对象来使用用户数据的另一个示例:
import FirebaseAuthUserContext from '...'; export default function CurrentUserIcon() { const userInfo = useContext(FirebaseAuthUserContext); switch (userInfo.dataStatus) { case "loaded": return ( <div class="user-icon"> <a href="/profile/settings"> <img src={userInfo.data.profileImage} /> <span>@{userInfo.data.username}</span> </a> </div> ); case "not-found": return ( <div class="user-icon"> <a href="/profile/settings"> <img src={PLACEHOLDER_IMAGE_URL} /> <span>New user</span> </a> </div> ); default: // unexpected status: loading/error/signed-out return null; } }
如果你正在使用 Typescript,你可以通过定义上下文可能处于的各种不同状态来帮助自己:
// at top of ./FirebaseAuthUserContext.jsx import type { User } from "firebase/auth"; import type { DocumentReference, FirestoreError } from "firebase/firestore"; export interface UserData { // shape of your user's data } interface UserDataInfo$Error { status: "error"; data: null; error: FirestoreError; } interface UserDataInfo$Loaded { status: "loaded"; data: UserData; ref: DocumentReference<UserData>; } interface UserDataInfo$Loading { status: "loading"; data?: undefined; } interface UserDataInfo$NotFound { status: "not-found"; data: null; ref: DocumentReference<UserData>; } interface UserDataInfo$SignedOut { status: "signed-out"; data: null; } type UserDataInfo = | UserDataInfo$Error | UserDataInfo$Loaded | UserDataInfo$Loading | UserDataInfo$NotFound | UserDataInfo$SignedOut; interface LiftedUserDataInfo<T extends UserDataInfo> { data: T["data"]; dataInfo: T; dataStatus: T["status"]; } interface FirebaseAuthUserContextType$Initializing extends LiftedUserDataInfo<UserDataInfo$Loading> { user: undefined; initializing: true; } interface FirebaseAuthUserContextType$SignedIn<T extends | UserDataInfo$Error | UserDataInfo$Loaded | UserDataInfo$Loading | UserDataInfo$NotFound > extends LiftedUserDataInfo<T> { user: User; initializing: false; } interface FirebaseAuthUserContextType$SignedOut extends LiftedUserDataInfo<UserDataInfo$SignedOut> { user: null; initializing: false; } export type FirebaseAuthUserContextType = | FirebaseAuthUserContextType$Initializing | FirebaseAuthUserContextType$SignedIn<UserDataInfo$Error> | FirebaseAuthUserContextType$SignedIn<UserDataInfo$Loaded> | FirebaseAuthUserContextType$SignedIn<UserDataInfo$Loading> | FirebaseAuthUserContextType$SignedIn<UserDataInfo$NotFound> | FirebaseAuthUserContextType$SignedOut; export const FirebaseAuthUserContext = createContext<FirebaseAuthUserContextType>({ data: undefined, dataInfo: { status: "loading" }, dataStatus: "loading", initializing: true, user: undefined } as FirebaseAuthUserContextType$Initializing);