Home > Software engineering >  How to load firebase data before the component renders in react?
How to load firebase data before the component renders in react?

Time:05-13

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 using getAuth() to fetch data in Profile.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:

  1. The PrivateRoute component isn't accessing the Auth object directly. It uses the useAuthStatus hook which itself also doesn't directly access the Auth object directly. The useAuthStatus hook uses the onAuthStateChanged function to "listen" for changes in the auth state.
  2. The checkingStatus state prevents the Profile 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 the loggedIn state when a user logs out.
  3. By the time a user has accessed the "/profile" route and logged in, the Firebase Auth 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:

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;

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 in Profile.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.

  • Related