如何在组件呈现之前加载 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?
您的带有受保护路由组件的代码的原始版本似乎有以下几个原因:
PrivateRoute
组件没有直接访问 Auth
对象。它使用 useAuthStatus
挂钩,它本身也不直接直接访问 Auth
对象。 useAuthStatus
挂钩使用 onAuthStateChanged
函数来“监听”身份验证状态的变化。
-
checkingStatus
状态会阻止呈现 Profile
组件,直到身份验证状态更改(用户已登录或注销)。 您的代码中实际上存在错误,当用户注销时不会更新 loggedIn
状态。
- 当用户访问
"/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 代码似乎都是同步的:
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;
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 会缓存大量数据以处理间歇性离线功能。
我有以下文件...
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 usinggetAuth()
to fetch data inProfile.jsx.
So why does the nested routes(original) code work and not this altered version?
您的带有受保护路由组件的代码的原始版本似乎有以下几个原因:
PrivateRoute
组件没有直接访问Auth
对象。它使用useAuthStatus
挂钩,它本身也不直接直接访问Auth
对象。useAuthStatus
挂钩使用onAuthStateChanged
函数来“监听”身份验证状态的变化。-
checkingStatus
状态会阻止呈现Profile
组件,直到身份验证状态更改(用户已登录或注销)。 您的代码中实际上存在错误,当用户注销时不会更新loggedIn
状态。 - 当用户访问
"/profile"
路由并登录时,FirebaseAuth
对象已经缓存了用户。
直接访问和呈现 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 代码似乎都是同步的:
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;
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 inProfile.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 会缓存大量数据以处理间歇性离线功能。