带有 React Hooks 的 Firebase 侦听器 [英] Firebase listener with React Hooks

查看:21
本文介绍了带有 React Hooks 的 Firebase 侦听器的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试弄清楚如何使用 Firebase 侦听器,以便使用 React 挂钩更新来刷新云 Firestore 数据.

最初,我使用带有 componentDidMount 函数的类组件来获取 firestore 数据.

this.props.firebase.db.collection('用户')//.doc(this.props.firebase.db.collection('users').doc(this.props.firebase.authUser.uid)).doc(this.props.firebase.db.collection('users').doc(this.props.authUser.uid)).得到().then(doc => {this.setState({ name: doc.data().name });//加载:假,});}

当页面更新时会中断,所以我想弄清楚如何移动侦听器以响应钩子.

我已经安装了 react-firebase-hooks 工具- 虽然我不知道如何阅读说明才能让它工作.

我有一个功能组件如下:

import React, { useState, useEffect } from 'react';从'react-firebase-hooks/firestore'导入{useDocument};进口 {BrowserRouter 作为路由器,路线,关联,转变,使用路由匹配,来自'反应路由器-dom';import * as ROUTES from '../../constants/Routes';从重构"导入 { compose };import { withFirebase } from '../Firebase/Index';import { AuthUserContext, withAuthorization, withEmailVerification, withAuthentication } from '../Session/Index';功能仪表板2(authUser){const FirestoreDocument = () =>{const [值,加载,错误] = useDocument(Firebase.db.doc(authUser.uid),//firebase.db.doc(authUser.uid),//firebase.firestore.doc(authUser.uid),{snapshotListenOptions: { includeMetadataChanges: true },});返回 (<div><p>{错误&&<strong>错误:{JSON.stringify(error)}</strong>}{加载&&<span>文档:正在加载...</span>}{价值&&<span>文档:{JSON.stringify(value.data())}</span>}</p>

);}}导出默认 withAuthentication(Dashboard2);

这个组件被封装在路由级别的 authUser 包装器中,如下所示:

(<AuthUserContext.Consumer>{ authUser =>(<Dashboard2 authUser={authUser} {...props}/>)}</AuthUserContext.Consumer>)}/>

我有一个 firebase.js 文件,它按如下方式插入 firestore:

class Firebase {构造函数(){app.initializeApp(config).firestore();/* 帮手 */this.fieldValue = app.firestore.FieldValue;/* Firebase API */this.auth = app.auth();this.db = app.firestore();}

它还定义了一个监听器来知道 authUser 何时发生变化:

onAuthUserListener(next, fallback) {//onUserDataListener(next, fallback) {返回 this.auth.onAuthStateChanged(authUser => {如果(身份验证用户){this.user(authUser.uid).得到().then(快照=> {让快照数据 = 快照数据();让用户数据 = {...snapshotData,//先是快照数据,这样它就不会覆盖来自 authUser 对象的信息uid: authUser.uid,电子邮件:authUser.email,emailVerified: authUser.emailVerifed,提供者数据:authUser.providerData};setTimeout(() => next(userData), 0);//转义这个 Promise 的错误处理程序}).catch(错误 => {//TODO: 处理错误?console.error('发生错误 -> ', err.code ? err.code + ': ' + err.message : (err.message || err));设置超时(回退,0);//转义这个 Promise 的错误处理程序});};如果(!authUser){//用户未登录,调用回退处理程序倒退();返回;}});};

然后,在我的 firebase 上下文设置中,我有:

import FirebaseContext, { withFirebase } from './Context';从'../../firebase' 导入 Firebase;导出默认 Firebase;导出 { FirebaseContext, withFirebase };

上下文在 withFirebase 包装器中设置如下:

从'react'导入React;const FirebaseContext = React.createContext(null);导出 const withFirebase = 组件 =>道具 =>(<FirebaseContext.Consumer>{firebase =><Component {...props} firebase={firebase}/>}</FirebaseContext.Consumer>);导出默认 FirebaseContext;

然后,在我的 withAuthentication HOC 中,我有一个上下文提供程序:

从'react'导入React;从 '../Session/Index' 导入 { AuthUserContext };import { withFirebase } from '../Firebase/Index';const withAuthentication = 组件 =>{类 WithAuthentication 扩展了 React.Component {构造函数(道具){超级(道具);this.state = {authUser:空,};}componentDidMount() {this.listener = this.props.firebase.auth.onAuthStateChanged(authUser =>{认证用户?this.setState({ authUser }): this.setState({ authUser: null });},);}componentWillUnmount() {this.listener();};使成为() {返回 (<AuthUserContext.Provider value={this.state.authUser}><组件{...this.props}/></AuthUserContext.Provider>);}}返回 withFirebase(WithAuthentication);};导出默认 withAuthentication;

目前 - 当我尝试此操作时,我在 Dashboard2 组件中收到一个错误消息:

<块引用>

Firebase' 未定义

我尝试了小写的 firebase 并得到同样的错误.

我也尝试过 firebase.firestore 和 Firebase.firestore.我得到了同样的错误.

我想知道我的 HOC 是否不能与函数组件一起使用?

我见过这个演示应用和这个博文.

按照博客中的建议,我创建了一个新的 firebase/contextReader.jsx:

 import React, { useEffect, useContext } from 'react';从'../../firebase' 导入 Firebase;export const userContext = React.createContext({用户:空,})export const useSession = () =>{const { 用户 } = useContext(userContext)返回用户}export const useAuth = () =>{const [state, setState] = React.useState(() =>{ const user = firebase.auth().currentUser返回 { 初始化:!用户,用户,}});功能 onChange(用户){setState({ 初始化: false, 用户 })}React.useEffect(() => {//监听认证状态变化const unsubscribe = firebase.auth().onAuthStateChanged(onChange)//卸载时取消订阅监听器返回 () =>取消订阅()}, [])返回状态}

然后我尝试将我的 App.jsx 包装在该阅读器中:

function App() {const { 初始化,用户 } = useAuth()如果(初始化){返回<div>加载</div>}//)//}//const App = () =>(返回 (<userContext.Provider value={{ user }}><路由器><导航/><Route path={ROUTES.LANDING} 精确组件={StandardLanding}/>

当我尝试这个时,我收到一条错误消息:

<块引用>

类型错误:_firebase__WEBPACK_IMPORTED_MODULE_2__.default.auth 不是一个函数

我已经看到这篇文章处理该错误并尝试卸载并重新安装纱线.没什么区别.

当我查看演示应用时,它建议应该使用接口"方法创建上下文.我不知道这是从哪里来的 - 我在文档中找不到解释它的参考.

我无法理解这些说明,只能尝试将其插入.

我看过这篇文章,它试图在不使用 react-firebase 的情况下收听 firestore-钩子.答案指向试图弄清楚如何使用这个工具.

我已阅读这个很好的解释 介绍如何从 HOC 转移到钩子.我被困在如何集成 Firebase 侦听器上.

我看过这篇文章如何考虑这样做的示例.不确定我是否应该尝试在 authListener componentDidMount 中执行此操作 - 或在尝试使用它的 Dashboard 组件中执行此操作.

下一次尝试我找到了这篇文章,它试图解决同样的问题.

当我尝试实施 Shubham Khatri 提供的解决方案时,我将 firebase 配置设置如下:

上下文提供者:从反应"导入反应,{useContext};从'../../firebase' 导入 Firebase;

const FirebaseContext = React.createContext();export const FirebaseProvider = (props) =>(<FirebaseContext.Provider value={new Firebase()}>{props.children}</FirebaseContext.Provider>);

上下文钩子有:

import React, { useEffect, useContext, useState } from 'react';const useFirebaseAuthentication = (firebase) =>{const [authUser, setAuthUser] = useState(null);useEffect(() =>{const 不听 =firebase.auth.onAuthStateChanged(authUser =>{认证用户?setAuthUser(authUser): setAuthUser(null);},);返回 () =>{不听();}});返回授权用户}导出默认 useFirebaseAuthentication;

然后在 index.js 中,我将应用程序包装在提供程序中:

从'react'导入React;从 'react-dom' 导入 ReactDOM;从'./components/App/Index'导入应用程序;从./components/Firebase/ContextHookProvider"导入 {FirebaseProvider};import * as serviceWorker from './serviceWorker';ReactDOM.render(<FirebaseProvider><应用程序/></FirebaseProvider>,document.getElementById('root'));serviceWorker.unregister();

然后,当我尝试在组件中使用侦听器时:

import React, {useContext} from 'react';从 '../Firebase/ContextHookProvider' 导入 { FirebaseContext };从'../Firebase/ContextHook' 导入 useFirebaseAuthentication;const Dashboard2 = (道具) =>{const firebase = useContext(FirebaseContext);const authUser =useFirebaseAuthentication(firebase);返回 (<div>authUser.email</div>)}导出默认 Dashboard2;

我尝试将其用作没有组件或身份验证包装器的路由:

当我尝试这个时,我收到一条错误消息:

<块引用>

尝试导入错误:FirebaseContext"未从中导出'../Firebase/ContextHookProvider'.

该错误消息是有道理的,因为 ContextHookProvider 不会导出 FirebaseContext - 它会导出 FirebaseProvider - 但如果我不尝试在 Dashboard2 中导入它 - 那么我将无法在尝试使用它的函数中访问它.

这种尝试的一个副作用是我的注册方法不再有效.它现在生成一条错误消息,内容为:

<块引用>

TypeError: 无法读取属性 'doCreateUserWithEmailAndPassword'空

我稍后会解决这个问题 - 但必须有一种方法来弄清楚如何使用 react with firebase 不涉及数月的这个循环,通过数百万无法获得基本身份验证设置的途径.是否有适用于反应钩子的 firebase (firestore) 入门套件?

下次尝试我试图遵循这个 udemy 课程 - 但它只能用于生成表单输入 - 没有监听器可以围绕经过身份验证的用户调整路线.

我尝试遵循此 youtube 教程 中的方法 - 其中有这个 repo 工作.它展示了如何使用钩子,但没有展示如何使用上下文.

下一次尝试我发现 this repo 似乎对在 firestore 中使用钩子有一个深思熟虑的方法.但是,我无法理解代码.

我克隆了它 - 并尝试添加所有公共文件,然后当我运行它时 - 我实际上无法让代码运行.我不确定如何让它运行的说明中缺少什么,以便查看代码中是否有可以帮助解决此问题的课程.

下一次尝试

我购买了 divjoy 模板,该模板标榜为 Firebase 设置(它不是为 firestore 设置的,以防其他人考虑将其作为选项).

该模板提出了一个 auth 包装器,用于初始化应用程序的配置 - 但仅用于 auth 方法 - 因此需要对其进行重组以允许另一个用于 firestore 的上下文提供程序.当你在这个过程中糊涂并使用 这篇文章,剩下的就是下面回调的错误:

useEffect(() => {const unsubscribe = firebase.auth().onAuthStateChanged(user => {如果(用户){设置用户(用户);} 别的 {设置用户(假);}});

它不知道firebase是什么.那是因为它是在 firebase 上下文提供程序中定义的,该提供程序被导入和定义(在 useProvideAuth() 函数中)为:

 const firebase = useContext(FirebaseContext)

没有机会回调,错误说:

<块引用>

React Hook useEffect 缺少依赖项:'firebase'.任何一个包括它或删除依赖数组

或者,如果我尝试将该常量添加到回调中,则会收到一条错误消息:

<块引用>

React Hook "useContext" 不能在回调中调用.反应钩子必须在 React 函数组件或自定义 React 中调用钩子函数

下一次尝试

我已将我的 firebase 配置文件缩减为仅配置变量(我将在我想使用的每个上下文的上下文提供程序中编写帮助程序).

从'firebase/app'导入firebase;导入'firebase/auth';导入'firebase/firestore';const devConfig = {apiKey:process.env.REACT_APP_DEV_API_KEY,authDomain:process.env.REACT_APP_DEV_AUTH_DOMAIN,数据库网址:process.env.REACT_APP_DEV_DATABASE_URL,projectId:process.env.REACT_APP_DEV_PROJECT_ID,存储桶:process.env.REACT_APP_DEV_STORAGE_BUCKET,messagesSenderId:process.env.REACT_APP_DEV_MESSAGING_SENDER_ID,appId:process.env.REACT_APP_DEV_APP_ID};const prodConfig = {apiKey:process.env.REACT_APP_PROD_API_KEY,authDomain:process.env.REACT_APP_PROD_AUTH_DOMAIN,数据库网址:process.env.REACT_APP_PROD_DATABASE_URL,projectId:process.env.REACT_APP_PROD_PROJECT_ID,存储桶:process.env.REACT_APP_PROD_STORAGE_BUCKET,消息发送者 ID:process.env.REACT_APP_PROD_MESSAGING_SENDER_ID,appId:process.env.REACT_APP_PROD_APP_ID};常量配置 =process.env.NODE_ENV === '生产' ?prodConfig : devConfig;类 Firebase {构造函数(){firebase.initializeApp(config);this.firebase = firebase;this.firestore = firebase.firestore();this.auth = firebase.auth();}};导出默认 Firebase;

然后我有一个身份验证上下文提供程序,如下所示:

import React, { useState, useEffect, useContext, createContext } from "react";从../firebase"导入 Firebase;const authContext = createContext();//包装应用程序并生成身份验证对象的提供程序组件 ...//... 可用于调用 useAuth() 的任何子组件.导出函数 ProvideAuth({ children }) {const auth = useProvideAuth();return <authContext.Provider value={auth}>{children}</authContext.Provider>;}//挂钩子组件以获取 auth 对象 ...//...并在更改时更新.export const useAuth = () =>{返回 useContext(authContext);};//创建身份验证对象并处理状态的提供程序钩子函数 useProvideAuth() {const [user, setUser] = useState(null);const 注册 =(电子邮件,密码)=>{返回 Firebase.auth().createUserWithEmailAndPassword(email, password).then(响应 => {设置用户(响应.用户);返回 response.user;});};const signin = (email, password) =>{返回 Firebase.auth().signInWithEmailAndPassword(email, password).then(响应 => {设置用户(响应.用户);返回 response.user;});};const 退出 = () =>{返回 Firebase.auth().登出().then(() => {设置用户(假);});};const sendPasswordResetEmail = 电子邮件 =>{返回 Firebase.auth().sendPasswordResetEmail(email).then(() => {返回真;});};const confirmPasswordReset =(密码,代码)=>{//从查询字符串对象中获取代码const resetCode = 代码 ||getFromQueryString("oobCode");返回 Firebase.auth().confirmPasswordReset(resetCode, 密码).then(() => {返回真;});};//订阅用户挂载useEffect(() => {const unsubscribe = firebase.auth().onAuthStateChanged(user => {如果(用户){设置用户(用户);} 别的 {设置用户(假);}});//订阅取消订阅功能返回 () =>取消订阅();}, []);返回 {用户,报名,登入,登出,发送密码重置电子邮件,确认密码重置};}const getFromQueryString = 键 =>{返回 queryString.parse(window.location.search)[key];};

我还制作了一个 firebase 上下文提供程序,如下所示:

import React, { createContext } from 'react';从../../firebase"导入 Firebase;const FirebaseContext = createContext(null)导出 { FirebaseContext }导出默认值 ({ children }) =>{返回 (<FirebaseContext.Provider value={ Firebase }>{ 孩子们 }</FirebaseContext.Provider>)}

然后,在 index.js 中,我将应用程序包装在 firebase 提供程序中

ReactDom.render(<FirebaseProvider><应用程序/></FirebaseProvider>,document.getElementById("root"));serviceWorker.unregister();

在我的路线列表中,我已经将相关路线包装在身份验证提供程序中:

从react"导入React;从./index"导入IndexPage;从./../util/router.js"导入{交换机、路由、路由器};从./../util/auth.js"导入{ProvideAuth};功能应用(道具){返回 (<ProvideAuth><路由器><开关><路由精确路径="/" component={IndexPage}/><路线组件={({位置}) =>{返回 (

);}}/></开关></路由器></ProvideAuth>);}导出默认应用程序;

在这次特别的尝试中,我又回到了之前用这个错误标记的问题:

<块引用>

类型错误:_firebase__WEBPACK_IMPORTED_MODULE_2__.default.auth 不是一个函数

它指出身份验证提供者的这一行是造成问题的原因:

useEffect(() => {const unsubscribe = firebase.auth().onAuthStateChanged(user => {如果(用户){设置用户(用户);} 别的 {设置用户(假);}});

我尝试在 Firebase 中使用大写的 F,但它产生了同样的错误.

当我尝试 Tristan 的建议时,我删除了所有这些内容,并尝试将我的取消订阅方法定义为不听方法(我不知道他为什么不使用 firebase 语言 - 但如果他的方法有效,我'd 更努力地找出原因).当我尝试使用他的解决方案时,错误消息说:

<块引用>

类型错误:_util_contexts_Firebase__WEBPACK_IMPORTED_MODULE_8___default(...) 不是函数

这篇文章的答案建议删除() 来自认证后.当我尝试这样做时,我收到一条错误消息:

<块引用>

TypeError: 无法读取未定义的属性onAuthStateChanged"

但是这篇文章表明在身份验证中导入 firebase 的方式存在问题文件.

我将其导入为:从../firebase"导入 Firebase;

Firebase 是类的名称.

Tristan 推荐的视频是有用的背景,但我目前正在播放第 9 集,但仍未找到应该有助于解决此问题的部分.有谁知道在哪里可以找到吗?

下一次尝试接下来 - 仅尝试解决上下文问题 - 我已经导入了 createContext 和 useContext 并尝试使用它们,如本文档中所示.

我无法通过以下错误消息:

<块引用>

错误:无效的挂钩调用.Hooks 只能在 body 内部调用一个功能组件.这可能发生在以下情况之一原因:...

我已经通过了此链接中的建议尝试和解决这个问题,无法弄清楚.我没有遇到此故障排除指南中显示的任何问题.

当前 - 上下文语句如下所示:

import React, { useContext } from 'react';从../../firebase"导入 Firebase;export const FirebaseContext = React.createContext();export const useFirebase = useContext(FirebaseContext);export const FirebaseProvider = props =>(<FirebaseContext.Provider value={new Firebase()}>{props.children}</FirebaseContext.Provider>);

我花时间使用这个 udemy course 来尝试找出上下文并hooks element to this question - 在观看之后 - 下面 Tristan 提出的解决方案的唯一方面是在他的帖子中没有正确调用 createContext 方法.它需要是React.createContext",但它仍然无法解决问题.

我还是卡住了.

谁能看到这里出了什么问题?

解决方案

主要修改:花了一些时间对此进行了更多研究,这是我提出的一个更简洁的解决方案,有人可能不同意我的观点,认为这是解决此问题的好方法.

使用Firebase Auth Hook

import { useEffect, useState, useCallback } from 'react';从firebase/app"导入 firebase;导入'firebase/auth';const firebaseConfig = {apiKey: "xxxxxxxxxxxxxxx",authDomain: "xxxx.firebaseapp.com",databaseURL: "https://xxxx.firebaseio.com",项目编号:xxxx",存储桶:xxxx.appspot.com",messagesSenderId: "xxxxxxxx",appId: "1:xxxxxxxxxx:web:xxxxxxxxx"};firebase.initializeApp(firebaseConfig)const useFirebase = () =>{const [authUser, setAuthUser] = useState(firebase.auth().currentUser);useEffect(() => {const unsubscribe = firebase.auth().onAuthStateChanged((user) => setAuthUser(user))返回 () =>{取消订阅()};}, []);const login = useCallback((email, password) => firebase.auth().signInWithEmailAndPassword(email, password), []);const logout = useCallback(() => firebase.auth().signOut(), [])返回{登录,authUser,注销}}导出 { useFirebase }

如果 authUser 为 null 则不认证,如果用户有值,则认证.

firebaseConfig 可以在 firebase 控制台 => 项目设置 => 应用 => >配置单选按钮

useEffect(() => {const unsubscribe = firebase.auth().onAuthStateChanged(setAuthUser)返回 () =>{取消订阅()};}, []);

这个 useEffect 钩子是跟踪用户 authChanges 的核心.我们向 firebase.auth() 的 onAuthStateChanged 事件添加一个监听器,用于更新 authUser 的值.此方法返回取消订阅此侦听器的回调,我们可以使用它在刷新 useFirebase 挂钩时清理侦听器.

这是我们进行 firebase 身份验证所需的唯一钩子(可以为 firestore 等制作其他钩子.

const App = () =>{const { 登录,authUser,注销} = useFirebase();如果(身份验证用户){返回 

<label>用户已通过身份验证</label><button onClick={logout}>Logout</button>

}const handleLogin = () =>{登录(name@email.com",password0");}返回

<label>用户未通过身份验证</label><button onClick={handleLogin}>登录</button>

}

这是 create-react-app

App 组件的基本实现

使用Firestore 数据库挂钩

const useFirestore = () =>{const getDocument = (documentPath, onUpdate) =>{firebase.firestore().doc(文档路径).onSnapshot(onUpdate);}const saveDocument = (documentPath, document) =>{firebase.firestore().doc(文档路径).set(文件);}const getCollection = (collectionPath, onUpdate) =>{firebase.firestore().collection(collectionPath).onSnapshot(onUpdate);}const saveCollection = (collectionPath, collection) =>{firebase.firestore().collection(collectionPath).set(集合)}返回 { getDocument, saveDocument, getCollection, saveCollection }}

这可以在您的组件中实现,如下所示:

const firestore = useFirestore();const [文档,setDocument] = useState();const handleGet = () =>{firestore.getDocument("测试/ItsWFgksrBvsDbx1ttTf",(结果) =>设置文档(结果.数据()));}const handleSave = () =>{firestore.saveDocument("测试/ItsWFgksrBvsDbx1ttTf",{ ...文档,新字段:你好"});

}

这消除了对 React useContext 的需要,因为我们直接从 firebase 本身获取更新.

注意几点:

  1. 保存未更改的文档不会触发新快照,因此过度保存"不会导致重新渲染
  2. 在调用 getDocument 时,回调 onUpdate 会立即被调用,并带有初始快照",因此您不需要额外的代码来获取文档的初始状态.

编辑删除了大部分旧答案

I am trying to figure out how to use a Firebase listener so that cloud firestore data is refreshed with react hooks updates.

Initially, I made this using a class component with a componentDidMount function to get the firestore data.

this.props.firebase.db
    .collection('users')
    // .doc(this.props.firebase.db.collection('users').doc(this.props.firebase.authUser.uid))
.doc(this.props.firebase.db.collection('users').doc(this.props.authUser.uid))
.get()
.then(doc => {
    this.setState({ name: doc.data().name });
    // loading: false,
  });  
}

That breaks when the page updates, so I am trying to figure out how to move the listener to react hooks.

I have installed the react-firebase-hooks tool - although I can't figure out how to read the instructions to be able to get it to work.

I have a function component as follows:

import React, { useState, useEffect } from 'react';
import { useDocument } from 'react-firebase-hooks/firestore';

import {
    BrowserRouter as Router,
    Route,
    Link,
    Switch,
    useRouteMatch,
 } from 'react-router-dom';
import * as ROUTES from '../../constants/Routes';
import { compose } from 'recompose';
import { withFirebase } from '../Firebase/Index';
import { AuthUserContext, withAuthorization, withEmailVerification, withAuthentication } from '../Session/Index';

function Dashboard2(authUser) {
    const FirestoreDocument = () => {

        const [value, loading, error] = useDocument(
          Firebase.db.doc(authUser.uid),
          //firebase.db.doc(authUser.uid),
          //firebase.firestore.doc(authUser.uid),
          {
            snapshotListenOptions: { includeMetadataChanges: true },
          }
        );
    return (

        <div>    



                <p>
                    {error && <strong>Error: {JSON.stringify(error)}</strong>}
                    {loading && <span>Document: Loading...</span>}
                    {value && <span>Document: {JSON.stringify(value.data())}</span>}
                </p>




        </div>

    );
  }
}

export default withAuthentication(Dashboard2);

This component is wrapped in an authUser wrapper at the route level as follows:

<Route path={ROUTES.DASHBOARD2} render={props => (
          <AuthUserContext.Consumer>
             { authUser => ( 
                <Dashboard2 authUser={authUser} {...props} />  
             )}
          </AuthUserContext.Consumer>
        )} />

I have a firebase.js file, which plugs into firestore as follows:

class Firebase {
  constructor() {
    app.initializeApp(config).firestore();
    /* helpers */
    this.fieldValue = app.firestore.FieldValue;


    /* Firebase APIs */
    this.auth = app.auth();
    this.db = app.firestore();


  }

It also defines a listener to know when the authUser changes:

onAuthUserListener(next, fallback) {
    // onUserDataListener(next, fallback) {
      return this.auth.onAuthStateChanged(authUser => {
        if (authUser) {
          this.user(authUser.uid)
            .get()
            .then(snapshot => {
            let snapshotData = snapshot.data();

            let userData = {
              ...snapshotData, // snapshotData first so it doesn't override information from authUser object
              uid: authUser.uid,
              email: authUser.email,
              emailVerified: authUser.emailVerifed,
              providerData: authUser.providerData
            };

            setTimeout(() => next(userData), 0); // escapes this Promise's error handler
          })

          .catch(err => {
            // TODO: Handle error?
            console.error('An error occured -> ', err.code ? err.code + ': ' + err.message : (err.message || err));
            setTimeout(fallback, 0); // escapes this Promise's error handler
          });

        };
        if (!authUser) {
          // user not logged in, call fallback handler
          fallback();
          return;
        }
    });
  };

Then, in my firebase context setup I have:

import FirebaseContext, { withFirebase } from './Context';
import Firebase from '../../firebase';
export default Firebase;
export { FirebaseContext, withFirebase };

The context is setup in a withFirebase wrapper as follows:

import React from 'react';
const FirebaseContext = React.createContext(null);

export const withFirebase = Component => props => (
  <FirebaseContext.Consumer>
    {firebase => <Component {...props} firebase={firebase} />}
  </FirebaseContext.Consumer>
);
export default FirebaseContext;

Then, in my withAuthentication HOC, I have a context provider as:

import React from 'react';
import { AuthUserContext } from '../Session/Index';
import { withFirebase } from '../Firebase/Index';

const withAuthentication = Component => {
  class WithAuthentication extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        authUser: null,
      };  
    }

    componentDidMount() {
      this.listener = this.props.firebase.auth.onAuthStateChanged(
        authUser => {
           authUser
            ? this.setState({ authUser })
            : this.setState({ authUser: null });
        },
      );
    }

    componentWillUnmount() {
      this.listener();
    };  

    render() {
      return (
        <AuthUserContext.Provider value={this.state.authUser}>
          <Component {...this.props} />
        </AuthUserContext.Provider>
      );
    }
  }
  return withFirebase(WithAuthentication);

};
export default withAuthentication;

Currently - when I try this, I get an error in the Dashboard2 component that says:

Firebase' is not defined

I tried lowercase firebase and get the same error.

I also tried firebase.firestore and Firebase.firestore. I get the same error.

I'm wondering if I can't use my HOC with a function component?

I have seen this demo app and this blog post.

Following the advice in the blog, I made a new firebase/contextReader.jsx with:

 import React, { useEffect, useContext } from 'react';
import Firebase from '../../firebase';



export const userContext = React.createContext({
    user: null,
  })

export const useSession = () => {
    const { user } = useContext(userContext)
    return user
  }

  export const useAuth = () => {
    const [state, setState] = React.useState(() => 
        { const user = firebase.auth().currentUser 
            return { initializing: !user, user, } 
        }
    );
    function onChange(user) {
      setState({ initializing: false, user })
    }

    React.useEffect(() => {
      // listen for auth state changes
      const unsubscribe = firebase.auth().onAuthStateChanged(onChange)
      // unsubscribe to the listener when unmounting
      return () => unsubscribe()
    }, [])

    return state
  }  

Then I try to wrap my App.jsx in that reader with:

function App() {
  const { initializing, user } = useAuth()
  if (initializing) {
    return <div>Loading</div>
  }

    // )
// }
// const App = () => (
  return (
    <userContext.Provider value={{ user }}> 


    <Router>
        <Navigation />
        <Route path={ROUTES.LANDING} exact component={StandardLanding} />

When I try this, I get an error that says:

TypeError: _firebase__WEBPACK_IMPORTED_MODULE_2__.default.auth is not a function

I have seen this post dealing with that error and have tried uninstalling and reinstalling yarn. It makes no difference.

When I look at the demo app, it suggests that context should be created using an 'interface' method. I can't see where this is coming from - I can't find a reference to explain it in the documentation.

I can't make sense of the instructions other than to try what I have done to plug this in.

I have seen this post which attempts to listen to firestore without using react-firebase-hooks. The answers point back to trying to figure out how to use this tool.

I have read this excellent explanation which goes into how to move away from HOCs to hooks. I'm stuck with how to integrate the firebase listener.

I have seen this post which provides a helpful example for how to think about doing this. Not sure if I should be trying to do this in the authListener componentDidMount - or in the Dashboard component that is trying to use it.

NEXT ATTEMPT I found this post, which is trying to solve the same problem.

When I try to implement the solution offered by Shubham Khatri, I set up the firebase config as follows:

A context provider with: import React, {useContext} from 'react'; import Firebase from '../../firebase';

const FirebaseContext = React.createContext(); 

export const FirebaseProvider = (props) => ( 
   <FirebaseContext.Provider value={new Firebase()}> 
      {props.children} 
   </FirebaseContext.Provider> 
); 

The context hook then has:

import React, { useEffect, useContext, useState } from 'react';

const useFirebaseAuthentication = (firebase) => {
    const [authUser, setAuthUser] = useState(null);

    useEffect(() =>{
       const unlisten = 
firebase.auth.onAuthStateChanged(
          authUser => {
            authUser
              ? setAuthUser(authUser)
              : setAuthUser(null);
          },
       );
       return () => {
           unlisten();
       }
    });

    return authUser
}

export default useFirebaseAuthentication;

Then in the index.js I wrap the App in the provider as:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App/Index';
import {FirebaseProvider} from './components/Firebase/ContextHookProvider';

import * as serviceWorker from './serviceWorker';


ReactDOM.render(

    <FirebaseProvider> 
    <App /> 
    </FirebaseProvider>,
    document.getElementById('root')
);

    serviceWorker.unregister();

Then, when I try to use the listener in the component I have:

import React, {useContext} from 'react';
import { FirebaseContext } from '../Firebase/ContextHookProvider';
import useFirebaseAuthentication from '../Firebase/ContextHook';


const Dashboard2 = (props) => {
    const firebase = useContext(FirebaseContext);
    const authUser = 
useFirebaseAuthentication(firebase);

    return (
        <div>authUser.email</div>
    )
 }

 export default Dashboard2;

And I try to use it as a route with no components or auth wrapper:

<Route path={ROUTES.DASHBOARD2} component={Dashboard2} />

When I try this, I get an error that says:

Attempted import error: 'FirebaseContext' is not exported from '../Firebase/ContextHookProvider'.

That error message makes sense, because ContextHookProvider does not export FirebaseContext - it exports FirebaseProvider - but if I don't try to import this in Dashboard2 - then I can't access it in the function that tries to use it.

One side effect of this attempt is that my sign up method no longer works. It now generates an error message that says:

TypeError: Cannot read property 'doCreateUserWithEmailAndPassword' of null

I'll solve this problem later- but there must be a way to figure out how to use react with firebase that does not involve months of this loop through millions of avenues that don't work to get a basic auth setup. Is there a starter kit for firebase (firestore) that works with react hooks?

Next attempt I tried to follow the approach in this udemy course- but it only works to generate a form input - there isn't a listener to put around the routes to adjust with the authenticated user.

I tried to follow the approach in this youtube tutorial - which has this repo to work from. It shows how to use hooks, but not how to use context.

NEXT ATTEMPT I found this repo that seems to have a well thought out approach to using hooks with firestore. However, I can't make sense of the code.

I cloned this - and tried to add all the public files and then when I run it - I can't actually get the code to operate. I'm not sure what's missing from the instructions for how to get this to run in order to see if there are lessons in the code that can help solve this problem.

NEXT ATTEMPT

I bought the divjoy template, which is advertised as being setup for firebase (it isn't setup for firestore in case anyone else is considering this as an option).

That template proposes an auth wrapper that initialises the config of the app - but just for the auth methods - so it needs to be restructured to allow another context provider for firestore. When you muddle through that process and use the process shown in this post, what's left is an error in the following callback:

useEffect(() => {
    const unsubscribe = firebase.auth().onAuthStateChanged(user => {
      if (user) {
        setUser(user);
      } else {
        setUser(false);
      }
    });

It doesn't know what firebase is. That's because it's defined in the firebase context provider which is imported and defined (in the useProvideAuth() function) as:

  const firebase = useContext(FirebaseContext)

Without chances to the callback, the error says:

React Hook useEffect has a missing dependency: 'firebase'. Either include it or remove the dependency array

Or, if I try and add that const to the callback, I get an error that says:

React Hook "useContext" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function

NEXT ATTEMPT

I have reduced my firebase config file down to just config variables (I will write helpers in the context providers for each context I want to use).

import firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/firestore';

const devConfig = {
    apiKey: process.env.REACT_APP_DEV_API_KEY,
    authDomain: process.env.REACT_APP_DEV_AUTH_DOMAIN,
    databaseURL: process.env.REACT_APP_DEV_DATABASE_URL,
    projectId: process.env.REACT_APP_DEV_PROJECT_ID,
    storageBucket: process.env.REACT_APP_DEV_STORAGE_BUCKET,
    messagingSenderId: process.env.REACT_APP_DEV_MESSAGING_SENDER_ID,
    appId: process.env.REACT_APP_DEV_APP_ID

  };


  const prodConfig = {
    apiKey: process.env.REACT_APP_PROD_API_KEY,
    authDomain: process.env.REACT_APP_PROD_AUTH_DOMAIN,
    databaseURL: process.env.REACT_APP_PROD_DATABASE_URL,
    projectId: process.env.REACT_APP_PROD_PROJECT_ID,
    storageBucket: process.env.REACT_APP_PROD_STORAGE_BUCKET,
    messagingSenderId: 
process.env.REACT_APP_PROD_MESSAGING_SENDER_ID,
    appId: process.env.REACT_APP_PROD_APP_ID
  };

  const config =
    process.env.NODE_ENV === 'production' ? prodConfig : devConfig;


class Firebase {
  constructor() {
    firebase.initializeApp(config);
    this.firebase = firebase;
    this.firestore = firebase.firestore();
    this.auth = firebase.auth();
  }
};

export default Firebase;  

I then have an auth context provider as follows:

import React, { useState, useEffect, useContext, createContext } from "react";
import Firebase from "../firebase";

const authContext = createContext();

// Provider component that wraps app and makes auth object ...
// ... available to any child component that calls useAuth().
export function ProvideAuth({ children }) {
  const auth = useProvideAuth();

  return <authContext.Provider value={auth}>{children}</authContext.Provider>;
}

// Hook for child components to get the auth object ...
// ... and update when it changes.
export const useAuth = () => {

  return useContext(authContext);
};

// Provider hook that creates auth object and handles state
function useProvideAuth() {
  const [user, setUser] = useState(null);


  const signup = (email, password) => {
    return Firebase
      .auth()
      .createUserWithEmailAndPassword(email, password)
      .then(response => {
        setUser(response.user);
        return response.user;
      });
  };

  const signin = (email, password) => {
    return Firebase
      .auth()
      .signInWithEmailAndPassword(email, password)
      .then(response => {
        setUser(response.user);
        return response.user;
      });
  };



  const signout = () => {
    return Firebase
      .auth()
      .signOut()
      .then(() => {
        setUser(false);
      });
  };

  const sendPasswordResetEmail = email => {
    return Firebase
      .auth()
      .sendPasswordResetEmail(email)
      .then(() => {
        return true;
      });
  };

  const confirmPasswordReset = (password, code) => {
    // Get code from query string object
    const resetCode = code || getFromQueryString("oobCode");

    return Firebase
      .auth()
      .confirmPasswordReset(resetCode, password)
      .then(() => {
        return true;
      });
  };

  // Subscribe to user on mount
  useEffect(() => {

    const unsubscribe = firebase.auth().onAuthStateChanged(user => {
      if (user) {
        setUser(user);
      } else {
        setUser(false);
      }
    });

    // Subscription unsubscribe function
    return () => unsubscribe();
  }, []);

  return {
    user,
    signup,
    signin,
    signout,
    sendPasswordResetEmail,
    confirmPasswordReset
  };
}

const getFromQueryString = key => {
  return queryString.parse(window.location.search)[key];
};

I also made a firebase context provider as follows:

import React, { createContext } from 'react';
import Firebase from "../../firebase";

const FirebaseContext = createContext(null)
export { FirebaseContext }


export default ({ children }) => {

    return (
      <FirebaseContext.Provider value={ Firebase }>
        { children }
      </FirebaseContext.Provider>
    )
  }

Then, in index.js I wrap the app in the firebase provider

ReactDom.render(
    <FirebaseProvider>
        <App />
    </FirebaseProvider>, 
document.getElementById("root"));

serviceWorker.unregister();

and in my routes list, I have wrapped the relevant routes in the auth provider:

import React from "react";
import IndexPage from "./index";
import { Switch, Route, Router } from "./../util/router.js";

import { ProvideAuth } from "./../util/auth.js";

function App(props) {
  return (
    <ProvideAuth>
      <Router>
        <Switch>
          <Route exact path="/" component={IndexPage} />

          <Route
            component={({ location }) => {
              return (
                <div
                  style={{
                    padding: "50px",
                    width: "100%",
                    textAlign: "center"
                  }}
                >
                  The page <code>{location.pathname}</code> could not be found.
                </div>
              );
            }}
          />
        </Switch>
      </Router>
    </ProvideAuth>
  );
}

export default App;

On this particular attempt, I'm back to the problem flagged earlier with this error:

TypeError: _firebase__WEBPACK_IMPORTED_MODULE_2__.default.auth is not a function

It points to this line of the auth provider as creating the problem:

useEffect(() => {

    const unsubscribe = firebase.auth().onAuthStateChanged(user => {
      if (user) {
        setUser(user);
      } else {
        setUser(false);
      }
    });

I have tried using capitalised F in Firebase and it generates the same error.

When I try Tristan's advice, I remove all of those things and try and define my unsubscribe method as an unlisten method (I don't know why he isn't using the firebase language - but if his approach worked, I'd try harder to figure out why). When I try to use his solution, the error message says:

TypeError: _util_contexts_Firebase__WEBPACK_IMPORTED_MODULE_8___default(...) is not a function

The answer to this post suggests removing () from after auth. When I try that, I get an error that says:

TypeError: Cannot read property 'onAuthStateChanged' of undefined

However this post suggest a problem with the way firebase is imported in the auth file.

I have it imported as: import Firebase from "../firebase";

Firebase is the name of the class.

The videos Tristan recommended are helpful background, but I'm currently on episode 9 and still not found the part that is supposed to help solve this problem. Does anyone know where to find that?

NEXT ATTEMPT Next - and trying to solve the context problem only - I have imported both createContext and useContext and tried to use them as shown in this documentation.

I can't get passed an error that says:

Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons: ...

I have been through the suggestions in this link to try and solve this problem and cannot figure it out. I don't have any of the problems shown in this trouble shooting guide.

Currently - the context statement looks as follows:

import React, {  useContext } from 'react';
import Firebase from "../../firebase";


  export const FirebaseContext = React.createContext();

  export const useFirebase = useContext(FirebaseContext);

  export const FirebaseProvider = props => (
    <FirebaseContext.Provider value={new Firebase()}>
      {props.children}
    </FirebaseContext.Provider>
  );  

I spent time using this udemy course to try and figure out the context and hooks element to this problem - after watching it - the only aspect to the solution proposed by Tristan below is that the createContext method isn't called correctly in his post. it needs to be "React.createContext" but it still doesn't get anywhere close to solving the problem.

I'm still stuck.

Can anyone see what's gone awry here?

解决方案

Major Edit: Took some time to look into this a bit more this is what I have come up with is a cleaner solution, someone might disagree with me about this being a good way to approach this.

UseFirebase Auth Hook

import { useEffect, useState, useCallback } from 'react';
import firebase from 'firebase/app';
import 'firebase/auth';

const firebaseConfig = {
  apiKey: "xxxxxxxxxxxxxx",
  authDomain: "xxxx.firebaseapp.com",
  databaseURL: "https://xxxx.firebaseio.com",
  projectId: "xxxx",
  storageBucket: "xxxx.appspot.com",
  messagingSenderId: "xxxxxxxx",
  appId: "1:xxxxxxxxxx:web:xxxxxxxxx"
};

firebase.initializeApp(firebaseConfig)

const useFirebase = () => {
  const [authUser, setAuthUser] = useState(firebase.auth().currentUser);

  useEffect(() => {
    const unsubscribe = firebase.auth()
      .onAuthStateChanged((user) => setAuthUser(user))
    return () => {
      unsubscribe()
    };
  }, []);

  const login = useCallback((email, password) => firebase.auth()
    .signInWithEmailAndPassword(email, password), []);

  const logout = useCallback(() => firebase.auth().signOut(), [])

  return { login, authUser, logout }
}

export { useFirebase }

If authUser is null then not authenticated, if user has a value, then authenticated.

firebaseConfig can be found on the firebase Console => Project Settings => Apps => Config Radio Button

useEffect(() => {
  const unsubscribe = firebase.auth()
    .onAuthStateChanged(setAuthUser)

  return () => {
    unsubscribe()
  };
}, []);

This useEffect hook is the core to tracking the authChanges of a user. We add a listener to the onAuthStateChanged event of firebase.auth() that updates the value of authUser. This method returns a callback for unsubscribing this listener which we can use to clean up the listener when the useFirebase hook is refreshed.

This is the only hook we need for firebase authentication (other hooks can be made for firestore etc.

const App = () => {
  const { login, authUser, logout } = useFirebase();

  if (authUser) {
    return <div>
      <label>User is Authenticated</label>
      <button onClick={logout}>Logout</button>
    </div>
  }

  const handleLogin = () => {
    login("name@email.com", "password0");
  }

  return <div>
    <label>User is not Authenticated</label>
    <button onClick={handleLogin}>Log In</button>
  </div>
}

This is a basic implementation of the App component of a create-react-app

useFirestore Database Hook

const useFirestore = () => {
  const getDocument = (documentPath, onUpdate) => {
    firebase.firestore()
      .doc(documentPath)
      .onSnapshot(onUpdate);
  }

  const saveDocument = (documentPath, document) => {
    firebase.firestore()
      .doc(documentPath)
      .set(document);
  }

  const getCollection = (collectionPath, onUpdate) => {
    firebase.firestore()
      .collection(collectionPath)
      .onSnapshot(onUpdate);
  }

  const saveCollection = (collectionPath, collection) => {
    firebase.firestore()
      .collection(collectionPath)
      .set(collection)
  }

  return { getDocument, saveDocument, getCollection, saveCollection }
}

This can be implemented in your component like so:

const firestore = useFirestore();
const [document, setDocument] = useState();

const handleGet = () => {
  firestore.getDocument(
    "Test/ItsWFgksrBvsDbx1ttTf", 
    (result) => setDocument(result.data())
  );
}

const handleSave = () => {
  firestore.saveDocument(
    "Test/ItsWFgksrBvsDbx1ttTf", 
    { ...document, newField: "Hi there" }
  );

}

This then removes the need for the React useContext as we get updates directly from firebase itself.

Notice a couple of things:

  1. Saving an unchanged document does not trigger a new snapshot so "oversaving" doesn't cause rerenders
  2. On calling getDocument the callback onUpdate is called straight away with an initial "snapshot" so you don't need extra code for getting the initial state of the document.

Edit has removed a large chunk of the old answer

这篇关于带有 React Hooks 的 Firebase 侦听器的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆