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

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

问题描述

我试图弄清楚如何使用Firebase侦听器,以便使用react hooks更新来刷新云Firestore数据.

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

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,
  });  
}

在页面更新时会中断,因此我试图弄清楚如何移动侦听器以响应挂钩.

我已经安装了 react-firebase-hooks 工具-尽管我不知道如何阅读说明才能使其正常工作.

我具有如下功能组件:

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);

此组件在路由级别处包装在authUser包装器中,如下所示:

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

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

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


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


  }

它还定义了一个侦听器,以知道authUser何时更改:

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;
        }
    });
  };

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

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

上下文是在withFirebase包装器中设置的,如下所示:

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;

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

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;

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

Firebase'未定义

我尝试使用小写的Firebase并得到相同的错误.

我还尝试了firebase.firestore和Firebase.firestore.我遇到同样的错误.

我想知道我是否不能将HOC与功能组件一起使用?

我已经看到了这个演示应用和这个这篇文章处理了该错误,并尝试了卸载并重新安装纱线.没关系.

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

除了尝试将其插入后所做的工作之外,我无法理解其他说明.

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

我已阅读此出色的解释介绍了如何从HOC转移到钩子.我对如何集成Firebase侦听器感到困惑.

我看过这篇文章,它提供了有用的帮助如何考虑这样做的示例.不知道我是否应该在authListener componentDidMount-或试图使用它的仪表板组件中尝试这样做.

下次尝试 我发现了这篇文章 ,它正在尝试解决相同的问题.

当我尝试实施Shubham Khatri提供的解决方案时,我按如下所示设置了Firebase配置:

具有以下内容的上下文提供程序: 从'react'导入React,{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 unlisten = 
firebase.auth.onAuthStateChanged(
          authUser => {
            authUser
              ? setAuthUser(authUser)
              : setAuthUser(null);
          },
       );
       return () => {
           unlisten();
       }
    });

    return authUser
}

export default useFirebaseAuthentication;

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

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();

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

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;

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

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

尝试此操作时,出现错误消息:

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

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

此尝试的副作用是我的注册方法不再起作用.现在,它会生成一条错误消息,内容为:

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

稍后我将解决此问题-但必须有一种方法来弄清楚如何与不使用几个月的循环的Firebase一起使用数百万条无法获得基本身份验证设置的途径.是否有适用于React钩子的Firebase(firestore)入门套件?

下一次尝试 我尝试在此虚拟课程-,但只能用于生成表单输入-没有听众可以绕过路由以与经过身份验证的用户进行调整.

我试图按照此 youtube教程中的方法进行操作-其中包含此存储库开始工作.它显示了如何使用挂钩,但没有说明如何使用上下文.

下一个尝试 我发现此存储库似乎有一种经过深思熟虑的方法,可以在Firestore中使用钩子.但是,我无法理解代码.

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

下次尝试

我购买了divjoy模板,该模板的广告宣传是为Firebase设置(如果其他人考虑将其作为选项,则不是为Firestore设置).

该模板提出了一个auth包装器,用于初始化应用程序的配置-但仅用于auth方法-因此需要对其进行重组,以允许其他上下文提供程序用于firestore.当您混淆该过程并使用

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

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

下次尝试

我已将Firebase配置文件简化为仅配置变量(我将在上下文提供程序中为我要使用的每个上下文编写帮助程序).

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;  

然后我有一个auth上下文提供程序,如下所示:

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];
};

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

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>
    )
  }

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

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

serviceWorker.unregister();

在我的路由列表中,我已将相关路由包装在auth提供程序中:

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;

在这种特殊的尝试下,我回到了之前用此错误标记的问题:

TypeError:_firebase__WEBPACK_IMPORTED_MODULE_2 __.default.auth不是 一个功能

它指向创建问题的身份验证提供程序的这一行:

useEffect(() => {

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

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

当我尝试使用Tristan的建议时,我将所有这些内容删除并尝试将unsubscribe方法定义为unlisten方法(我不知道他为什么不使用firebase语言-但如果他的方法有效,我会d尽力找出原因).当我尝试使用他的解决方案时,错误消息显示:

TypeError: _util_contexts_Firebase__WEBPACK_IMPORTED_MODULE_8 ___ default(...)不是函数

此帖子的答案建议删除()来自授权后.尝试该操作时,出现错误消息:

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

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

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

Firebase是该类的名称.

特里斯坦(Tristan)推荐的视频是有帮助的背景,但是我目前在第9集,但仍找不到应该有助于解决此问题的部分.有人知道在哪里找到吗?

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

我无法通过错误提示:

错误:无效的挂接调用.钩子只能在身体内部调用 功能组件的名称.可能发生以下情况之一 原因:...

我已经通过此链接中的建议来尝试并解决这个问题,无法解决.我没有此疑难解答指南中显示的任何问题.

当前-上下文语句如下:

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>
  );  

我花了一些时间使用此 udemy课程来尝试弄清上下文并将元素挂钩到该问题上-在观看之后-Tristan提出的解决方案的唯一方面是,在他的帖子中未正确调用createContext方法.它必须是"React.createContext",但仍然无法解决问题.

我仍然被困住.

任何人都可以看到这里发生了什么问题吗?

解决方案

主要修改: 花了一些时间对此进行更多研究,这是我想出的一个更清洁的解决方案,可能有人不赞成我这是解决此问题的好方法.

使用Firebase身份验证挂钩

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 }

如果authUser为null,则不进行身份验证;如果user有值,则进行身份验证.

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

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

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

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

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

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>
}

这是create-react-app

App组件的基本实现.

使用Firestore数据库挂钩

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 }
}

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

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" }
  );

}

当我们直接从firebase本身获取更新时,这消除了对React useContext的需求.

注意几件事:

  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天全站免登陆