如何在组件呈现之前加载 firebase 数据?

How to load firebase data before the component renders in react?

我有以下文件...

useAuthStatus.js

import {useEffect, useState, useRef} from 'react';
import { getAuth, onAuthStateChanged } from 'firebase/auth';

const useAuthStatus = () => {
  const [loggedIn, setLoggedIn] = useState(false);
  const [checkingStatus, setCheckingStatus] = useState(true);
  const isMounted = useRef(true);

  
  useEffect(() => {
    if (isMounted) {
      const auth = getAuth();

      onAuthStateChanged(auth, (user) => {
        if (user) {
          setLoggedIn(true);          
        }

        setCheckingStatus(false);
      });
    }

    return () => {
      isMounted.current = false;
    }      
  }, [isMounted]);

  return {loggedIn, checkingStatus}
}

export default useAuthStatus

PrivateRoute.jsx

import React from 'react';
import { Navigate, Outlet } from 'react-router-dom';
import useAuthStatus from '../hooks/useAuthStatus';
import Spinner from './Spinner';

const PrivateRoute = () => {
  const {loggedIn, checkingStatus} = useAuthStatus();

  if (checkingStatus) {
      return <Spinner/>
  }

  return loggedIn ? <Outlet /> : <Navigate to='/sign-in' />
  
}

export default PrivateRoute

Profile.jsx

import React, {useState} from 'react';
import { db } from '../firebase.config';
import { getAuth, signOut, updateProfile } from 'firebase/auth';
import {doc, updateDoc} from 'firebase/firestore';
import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify';

const Profile = () => {
  const auth = getAuth();

  const [changeDetails, setChangeDetails] = useState(false);

  const [formData, setFormData] = useState({
    name: auth.currentUser.displayName,
    email: auth.currentUser.email
  });

  const {name, email} = formData;

  const navigate = useNavigate();

  const onLogOut = async () => {
    await signOut(auth);

    navigate('/');
  }

  const onSubmit = async () => {
    try {
      if (auth.currentUser.displayName !== name) {
        //update display name in firebase auth
        await updateProfile(auth.currentUser, {
          displayName: name
        });

        //update name in firestore
        const userRef = doc(db, 'users', auth.currentUser.uid);

        await updateDoc(userRef, {
          name: name
        })
      }
    } catch (error) {
      toast.error('Unable to change profile details');
    }
  }

  const onChange = (e) => {
    setFormData((prevState) => (
      {
        ...prevState,
        [e.target.id]: e.target.value
      }
    ))
  }


  return (
    <div className='profile'>
      <header className='profileHeader'>
        <p className='pageHeader'>My Profile</p>
        <button type='button' className='logOut' onClick={onLogOut}>Logout</button>
      </header>

      <main>
        <div className='profileDetailsHeader'>
          <p className='profileDetailsText'>Personal Details</p>
          <p className='changePersonalDetails' onClick={() => {
            changeDetails && onSubmit();
            setChangeDetails((prevState) => !prevState);
          }}>
            {changeDetails ? 'done' : 'change'}
          </p>
        </div>

        <div className='profileCard'>
          <form>
            <input type="text" id='name' className={!changeDetails ? 'profileName' : 'profileNameActive'} disabled={!changeDetails} value={name} onChange={onChange}/>

            <input type="text" id='email' className={!changeDetails ? 'profileEmail' : 'profileEmailActive'} disabled={!changeDetails} value={email} onChange={onChange}/>
          </form>
        </div>
      </main>
    </div>
  )
}

export default Profile

App.jsx

import React from "react";
import {BrowserRouter, Routes, Route} from 'react-router-dom';
import { ToastContainer } from "react-toastify";
import 'react-toastify/dist/ReactToastify.css';

import Navbar from "./components/Navbar";
import PrivateRoute from "./components/PrivateRoute";

import Explore from './pages/Explore';
import ForgotPassword from './pages/ForgotPassword';
import Offers from './pages/Offers';
import Profile from './pages/Profile';
import SignIn from './pages/SignIn';
import SignUp from './pages/SignUp';

function App() {
  return (
    <>
      <BrowserRouter>
        <Routes>
          <Route path="/" element={<Explore />}/>
          <Route path="/offers" element={<Offers />}/>
          
          <Route path="/profile" element={<PrivateRoute />}>
            <Route path="/profile" element={<Profile />} />
          </Route>
          
          <Route path="/sign-in" element={<SignIn />}/>
          <Route path="/sign-up" element={<SignUp />}/>
          <Route path="/forgot-password" element={<ForgotPassword />}/>
        </Routes>

        <Navbar />
      </BrowserRouter>

      <ToastContainer position="top-center" hideProgressBar={true} autoClose={3000} pauseOnHover={false}/>
    </>
  );
}

export default App;

这是当前的工作代码...在继续我的问题之前,我将简要解释发生了什么。当未经授权的用户访问“/profile”时,他们会被定向到 PrivateRoute 组件。如果用户已登录,则会呈现来自 React 路由器的 <Outlet/> 组件,然后呈现 Profile 组件。但是,如果用户未登录,则他们将被 PrivateRoute 重定向到“/登录”。另请注意 App.jsx.

中的嵌套路由

如果我从嵌套路由中删除 App.jsx 中的行 <Route path="/profile" element={<Profile />} /> 并使其成为正常路由,那么当加载 Profile 组件时,我会收到错误消息“TypeError:无法读取 null 的属性” .我相信我收到此错误是因为组件在 const auth = getAuth();(在 Profile.jsx 中)完成获取数据并在 useState() 中填充名称和电子邮件之前加载。

现在我的问题是,在 useAuthStatus.js 中,我使用 getAuth() 获取数据,然后再次使用 getAuth() 获取 Profile.jsx 中的数据。那么为什么嵌套路由(原始)代码可以工作而不是这个修改后的版本呢?如果我需要在 Profile.jsx 中再次使用 getAuth() 那么数据是如何在组件之前加载的呢?在嵌套路由中,如果外部“/profile”使用 getAuth() 那么该数据是否也以某种方式传输到嵌套路由?

好的,我想我现在已经理解你的问题了。

Now my question is, in useAuthStatus.js I am using getAuth() to fetch data then AGAIN I'm using getAuth() to fetch data in Profile.jsx. So why does the nested routes(original) code work and not this altered version?

您的带有受保护路由组件的代码的原始版本似乎有以下几个原因:

  1. PrivateRoute 组件没有直接访问 Auth 对象。它使用 useAuthStatus 挂钩,它本身也不直接直接访问 Auth 对象。 useAuthStatus 挂钩使用 onAuthStateChanged 函数来“监听”身份验证状态的变化。
  2. checkingStatus 状态会阻止呈现 Profile 组件,直到身份验证状态更改(用户已登录或注销)。 您的代码中实际上存在错误,当用户注销时不会更新 loggedIn 状态。
  3. 当用户访问 "/profile" 路由并登录时,Firebase Auth 对象已经缓存了用户。

直接访问和呈现 Profile 的更改版本似乎失败了,因为错误指出 Auth 对象上没有当前用户值。

Uncaught TypeError: Cannot read properties of null (reading 'displayName')

简介

const Profile = () => {
  const auth = getAuth();

  const [changeDetails, setChangeDetails] = useState(false);

  const [formData, setFormData] = useState({
    name: auth.currentUser.displayName, // auth.currentUser is null!
    email: auth.currentUser.email
  });

  ...

所有 firebase 代码似乎都是同步的:

getAuth

Returns the Auth instance associated with the provided FirebaseApp. If no instance exists, initializes an Auth instance with platform-specific default dependencies.

export declare function getAuth(app?: FirebaseApp): Auth;

Auth.currentUser

The currently signed-in user (or null).

readonly currentUser: User | null;

Auth.currentUser 对象要么是经过身份验证的用户对象,要么为空。 Profile 组件试图在组件安装之前访问此 currentUser 属性 以设置初始渲染的初始状态值。

可以 Auth.currentUser 属性 上使用 null-check/guard-clause 或可选链接运算符,并结合空值合并运算符来提供后备值:

const Profile = () => {
  const auth = getAuth();

  const [changeDetails, setChangeDetails] = useState(false);

  const [formData, setFormData] = useState({
    name: auth.currentUser?.displayName ?? "", // displayName or ""
    email: auth.currentUser?.email ?? ""       // email or ""
  });

  ...

但这只在组件安装时设置值,并且只有在有经过身份验证的用户时才设置值。最好坚持使用 onAuthStateChanged 方法来处理身份验证状态。

现在关于 loggedIn 错误:

const useAuthStatus = () => {
  const [loggedIn, setLoggedIn] = useState(false);
  const [checkingStatus, setCheckingStatus] = useState(true);

  useEffect(() => {
    let isMounted = true; // <-- use local isMounted variable
    
    const auth = getAuth();

    onAuthStateChanged(auth, (user) => {
      if (isMounted) { // <-- check if still mounted in callback
        setLoggedIn(!!user); // <-- coerce User | null to boolean
        setCheckingStatus(false);
      }
    });

    return () => {
      isMounted = false;
    }      
  }, []);

  return { loggedIn, checkingStatus };
};

If I need to use getAuth() again in Profile.jsx then how come the data loads BEFORE the component?

任何时候需要访问 Auth 对象时都需要使用 getAuth

In the nested routes if the outer "/profile" uses getAuth() then does that data get transferred to the nested route too somehow?

不是真的。更确切地说,您的应用程序有一个 Firebase 实例,它有一个被访问的 Auth 对象。这样,它更像是一个全局上下文。 Firebase 会缓存大量数据以处理间歇性离线功能。