I have the following files...
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;
This is the current working code...i'll briefly explain what's happening before proceeding to my question. When an unauthorized user visits "/profile" they get directed to PrivateRoute component. If the user is logged in then an <Outlet/>
component from react router gets rendered and then the Profile component get rendered. However, If the user is not logged in then they are redirected to "/sign-in" by PrivateRoute. Please also note the nested routes in App.jsx.
If I remove the line <Route path="/profile" element={<Profile />} />
in App.jsx from the nested route and make it a normal route then when the Profile component loads I get an error "TypeError: Cannot read properties of null". I believe I'm getting this error because the component is loading before const auth = getAuth();
(in Profile.jsx) has finished fetching the data and populating name and email in useState().
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? If I need to use getAuth() again in Profile.jsx then how come the data loads BEFORE the component? In the nested routes if the outer "/profile" uses getAuth() then does that data get transfered to the nested route too somehow?
CodePudding user response:
Ok, I think I've grokked what you are asking now.
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?
It seems the original version of your code with the protected route component worked for a few reasons:
- The
PrivateRoute
component isn't accessing theAuth
object directly. It uses theuseAuthStatus
hook which itself also doesn't directly access theAuth
object directly. TheuseAuthStatus
hook uses theonAuthStateChanged
function to "listen" for changes in the auth state. - The
checkingStatus
state prevents theProfile
component from being rendered until the auth status changes, either a user has logged in, or logged out. There's actually bug in your code that doesn't update theloggedIn
state when a user logs out. - By the time a user has accessed the
"/profile"
route and logged in, the FirebaseAuth
object has cached the user.
The altered version that directly accesses and renders Profile
seems to fail because there is no current user value on the Auth
object as the error points out.
Uncaught TypeError: Cannot read properties of null (reading 'displayName')
Profile
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
});
...
All the firebase code appears to be synchronous:
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;
The Auth.currentUser
object is either going to be an authenticated user object or null. The Profile
component is attempting to access this currentUser
property prior to the component mounting to set the initial state value for the initial render.
You could use a null-check/guard-clause or Optional Chaining Operator on the Auth.currentUser
property combined with a Nullish Coalescing Operator to provide a fallback value:
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 ""
});
...
But this only sets the value when the component mounts and only if there was an authenticated user. It's best to stick to using the onAuthStateChanged
method to handle the auth state.
Now about the loggedIn
bug:
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?
You need to use getAuth
any time you need to access the Auth
object.
In the nested routes if the outer "/profile" uses
getAuth()
then does that data get transferred to the nested route too somehow?
Not really. It is rather that your app has a single Firebase instance, which has a single Auth
object that is accessed. In this way it is more like a global context. Firebase does a fair amount of caching of data to handle intermittent offline capabilities.