Как загрузить данные 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

Профиль.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. Если пользователь вошел в систему, то визуализируется компонент <Outlet/> из реагирующего маршрутизатора, а затем визуализируется компонент профиля. Однако, если пользователь не вошел в систему, он перенаправляется на «/ sign-in» с помощью PrivateRoute. Также обратите внимание на вложенные маршруты в App.jsx.

Если я удалю строку <Route path = "/profile" element = {<Profile />} /> в App.jsx из вложенного маршрута и сделаю его обычным маршрутом, то при загрузке компонента Profile я получу ошибку «TypeError: Cannot read properties of null». Я полагаю, что получаю эту ошибку, потому что компонент загружается до того, как const auth = getAuth(); (в Profile.jsx) закончил получение данных и заполнение имени и адреса электронной почты в useState().

Теперь мой вопрос: в useAuthStatus.js я использую getAuth() для извлечения данных, а затем СНОВА я использую getAuth() для извлечения данных в Profile.jsx. Так почему же работает код вложенных маршрутов (исходный), а не эта измененная версия? Если мне нужно снова использовать getAuth() в Profile.jsx, то почему данные загружаются ДО компонента? Во вложенных маршрутах, если внешний «/ профиль» использует getAuth(), тогда эти данные каким-то образом передаются и во вложенный маршрут?

Неясно, что вы имеете в виду Кроме как, потому что есть какая-то проблема с getAuth, которую вы не включили в свой минимальный, полный и воспроизводимый пример кода, чтобы мы могли видеть, что он делает. Пожалуйста, отредактируйте свой пост, чтобы включить весь соответствующий код, с которым вы работаете или о котором спрашиваете. Пожалуйста, также включите полное сообщение об ошибке, чтобы мы могли видеть, к чему оно относится. Можете ли вы также уточнить, что именно вы просите? Похоже, вы в основном спрашиваете, почему не использовать PrivateRoute не работает.

Drew Reese 12.05.2022 21:28

@DrewReese Я включил полный код для файлов. Мой вопрос довольно прост. Мне нужно выяснить, почему текущий вложенный маршрут в App.jsx работает, а просто <Route path = "/profile" element = {<Profile />} /> не работает сам по себе (не во вложенном маршруте)... я получаю сообщение об ошибке "Uncaught TypeError: Cannot read properties of null (reading ' отображаемое имя')". Как упоминалось в вопросе, getAuth() снова вызывается в Profile.jsx для извлечения данных ... почему это так, если PrivateRoute уже проверил вход пользователя в систему?

falah mahmood 13.05.2022 03:49
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Навигация по приложениям React: Исчерпывающее руководство по React Router
Навигация по приложениям React: Исчерпывающее руководство по React Router
React Router стала незаменимой библиотекой для создания одностраничных приложений с навигацией в React. В этой статье блога мы подробно рассмотрим...
Массив зависимостей в React
Массив зависимостей в React
Все о массиве Dependency и его связи с useEffect.
1
2
29
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Хорошо, я думаю, что понял, о чем вы сейчас спрашиваете.

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 перед монтированием компонента, чтобы установить значение начального состояния для начального рендеринга.

Вы мог используете null-check/guard-clause или необязательный оператор цепочки для свойства Auth.currentUser в сочетании с нулевым оператором объединения, чтобы предоставить резервное значение:

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?

Вам нужно использовать getAuth каждый раз, когда вам нужно получить доступ к объекту Auth.

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 в значительной степени кэширует данные для обработки периодически возникающих автономных возможностей.

Большое спасибо за подробное объяснение. Теперь все начинает проясняться. Всего одна проблема: в последней части вы упомянули: «Ваше приложение имеет один экземпляр Firebase, к которому осуществляется доступ к одному объекту Auth. Таким образом, это больше похоже на глобальный контекст. Firebase выполняет достаточное количество кэширования данных» ...Правильно ли я предполагаю, что, поскольку getAuth() был доступен в useAuthStatus.js, следовательно, его копия была кэширована в приложении, и когда Profile.jsx СНОВА вызвал getAuth(), эта сохраненная копия использовалась для извлечения данных и заполнить переменные в useState()??

falah mahmood 13.05.2022 08:22

Кроме того, что касается ошибки loggedIn... не могли бы вы объяснить, почему вы решили использовать локальную переменную вместо useRef()? Будет ли в этом случае выбор одного над другим вызывать какие-либо проблемы?

falah mahmood 13.05.2022 08:35

@falahmahmood Я связал документы, getAuth «Возвращает экземпляр Auth, связанный с предоставленным FirebaseApp», поэтому после создания экземпляра Auth он всегда один и тот же.

Drew Reese 13.05.2022 08:36

@falahmahmood Об использовании локальной переменной поверх Ref. Скорее всего, было предупреждение React относительно isMounted ref, в котором говорилось что-то вроде «…возможного изменения значений перед запуском функции очистки хука useEffect, сначала сохраните копию и сошлитесь на нее…». Использование локальной переменной здесь более логично, так как это единственное место, где на нее нужно ссылаться. Это шаблон программирования, ограничивающий область переменных только тем, что им нужно.

Drew Reese 13.05.2022 08:39

Понятно! Большое вам спасибо за вашу помощь. Теперь все ясно.

falah mahmood 13.05.2022 08:41

Другие вопросы по теме