Ouvinte do Firebase com ganchos de reação

27

Estou tentando descobrir como usar um ouvinte do Firebase para que os dados do firestore na nuvem sejam atualizados com atualizações de ganchos de reação.

Inicialmente, fiz isso usando um componente de classe com uma função componentDidMount para obter os dados do 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,
  });  
}

Isso é interrompido quando a página é atualizada, então estou tentando descobrir como mover o ouvinte para reagir aos ganchos.

Instalei a ferramenta react-firebase-hooks - embora não consiga descobrir como ler as instruções para poder fazê-la funcionar.

Eu tenho um componente de função da seguinte maneira:

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

Este componente é agrupado em um wrapper authUser no nível da rota, da seguinte maneira:

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

Eu tenho um arquivo firebase.js, que se conecta ao firestore da seguinte maneira:

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


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


  }

Ele também define um ouvinte para saber quando o authUser é alterado:

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

Então, na minha configuração de contexto do firebase, tenho:

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

O contexto é configurado em um wrapper withFirebase da seguinte maneira:

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;

Então, no meu HOC de autenticação, tenho um provedor de contexto como:

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;

Atualmente - quando tento isso, recebo um erro no componente Dashboard2 que diz:

Firebase 'não está definido

Tentei firebase em minúsculas e recebi o mesmo erro.

Eu também tentei firebase.firestore e Firebase.firestore. Eu recebo o mesmo erro.

Gostaria de saber se não posso usar meu HOC com um componente de função?

Eu já vi esse aplicativo de demonstração e esta postagem no blog .

Seguindo os conselhos do blog, criei um novo firebase / contextReader.jsx com:

 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
  }  

Em seguida, tento envolver meu App.jsx nesse leitor com:

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

Quando tento isso, recebo um erro que diz:

TypeError: _firebase__WEBPACK_IMPORTED_MODULE_2 __. Default.auth não é uma função

Eu vi esse post lidando com esse erro e tentei desinstalar e reinstalar o fio. Não faz diferença.

Quando olho para o aplicativo de demonstração , ele sugere que o contexto deve ser criado usando um método de 'interface'. Não consigo ver de onde vem isso - não consigo encontrar uma referência para explicá-lo na documentação.

Não consigo entender as instruções além de tentar o que fiz para conectar isso.

Eu já vi esse post que tenta ouvir o firestore sem usar o react-firebase-hooks. As respostas apontam para a tentativa de descobrir como usar essa ferramenta.

Eu li esta excelente explicação que explica como se afastar dos HOCs para ganchos. Estou empolgado com como integrar o ouvinte do firebase.

Eu já vi esse post, que fornece um exemplo útil de como pensar em fazer isso. Não tenho certeza se devo tentar fazer isso no authListener componentDidMount - ou no componente Dashboard que está tentando usá-lo.

PRÓXIMA TENTATIVA Encontrei este post , que está tentando resolver o mesmo problema.

Quando tento implementar a solução oferecida por Shubham Khatri, instalei a configuração do firebase da seguinte maneira:

Um provedor de contexto com: import React, {useContext} de 'react'; importar Firebase de '../../firebase';

const FirebaseContext = React.createContext(); 

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

O gancho de contexto possui:

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;

Em seguida, no index.js, envolvo o aplicativo no provedor como:

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

Então, quando tento usar o ouvinte no componente, tenho:

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;

E eu tento usá-lo como uma rota sem componentes ou wrapper de autenticação:

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

Quando tento isso, recebo um erro que diz:

Tentativa de erro de importação: 'FirebaseContext' não é exportado de '../Firebase/ContextHookProvider'.

Essa mensagem de erro faz sentido, porque ContextHookProvider não exporta o FirebaseContext - exporta o FirebaseProvider - mas se eu não tentar importar isso no Dashboard2 - não posso acessá-lo na função que tenta usá-lo.

Um efeito colateral dessa tentativa é que meu método de inscrição não funciona mais. Agora ele gera uma mensagem de erro que diz:

TypeError: Não é possível ler a propriedade 'doCreateUserWithEmailAndPassword' de null

Vou resolver esse problema mais tarde - mas deve haver uma maneira de descobrir como usar o reagir com firebase que não envolva meses desse ciclo por milhões de avenidas que não funcionam para obter uma configuração básica de autenticação. Existe um kit inicial para firebase (firestore) que funcione com ganchos de reação?

Na próxima tentativa , tentei seguir a abordagem deste curso udemy - mas só funciona para gerar uma entrada de formulário - não há um ouvinte para contornar as rotas para ajustar com o usuário autenticado.

Eu tentei seguir a abordagem neste tutorial do youtube - que tem esse repositório para trabalhar. Ele mostra como usar ganchos, mas não como usar o contexto.

PRÓXIMA TENTATIVA Encontrei este repositório que parece ter uma abordagem bem pensada para usar ganchos com firestore. No entanto, não consigo entender o código.

Eu clonei isso - e tentei adicionar todos os arquivos públicos e, quando o executo -, na verdade, não consigo fazer com que o código funcione. Não tenho certeza do que está faltando nas instruções de como executar isso para verificar se há lições no código que podem ajudar a resolver esse problema.

PRÓXIMA TENTATIVA

Comprei o modelo divjoy, anunciado como sendo configurado para o firebase (não é configurado para o firestore no caso de alguém mais considerar isso como uma opção).

Esse modelo propõe um wrapper de autenticação que inicializa a configuração do aplicativo - mas apenas para os métodos de autenticação -, portanto, ele precisa ser reestruturado para permitir outro provedor de contexto para o firestore. Quando você percorre esse processo e usa o processo mostrado nesta postagem , resta um erro no seguinte retorno de chamada:

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

Não sabe o que é o firebase. Isso ocorre porque é definido no provedor de contexto do firebase que é importado e definido (na função useProvideAuth ()) como:

  const firebase = useContext(FirebaseContext)

Sem chances para o retorno de chamada, o erro diz:

O React Hook useEffect tem uma dependência ausente: 'firebase'. Inclua-o ou remova a matriz de dependência

Ou, se eu tentar adicionar essa const ao retorno de chamada, recebo um erro que diz:

O gancho de reação "useContext" não pode ser chamado dentro de um retorno de chamada. Ganchos de reação devem ser chamados em um componente da função React ou em uma função personalizada do React Hook

PRÓXIMA TENTATIVA

Reduzi meu arquivo de configuração do firebase para apenas variáveis ​​de configuração (escreverei ajudantes nos provedores de contexto para cada contexto que desejar usar).

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;  

Em seguida, tenho um provedor de contexto de autenticação da seguinte maneira:

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

Também fiz um provedor de contexto de base de firmas da seguinte maneira:

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

Em index.js, envolvo o aplicativo no provedor da base de firmas

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

serviceWorker.unregister();

e na minha lista de rotas, agrupei as rotas relevantes no provedor de autenticação:

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;

Nesta tentativa em particular, estou de volta ao problema sinalizado anteriormente com este erro:

TypeError: _firebase__WEBPACK_IMPORTED_MODULE_2 __. Default.auth não é uma função

Aponta para esta linha do provedor de autenticação como criando o problema:

useEffect(() => {

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

Eu tentei usar F maiúsculo no Firebase e ele gera o mesmo erro.

Quando tento o conselho de Tristan, removo todas essas coisas e tento definir meu método de cancelamento de inscrição como um método que não está listado (não sei por que ele não está usando a linguagem firebase - mas se a abordagem dele funcionasse, eu tentaria mais para descobrir o porquê). Quando tento usar sua solução, a mensagem de erro diz:

TypeError: _util_contexts_Firebase__WEBPACK_IMPORTED_MODULE_8 ___ padrão (...) não é uma função

A resposta a esta postagem sugere remover () após a autenticação. Quando tento isso, recebo um erro que diz:

TypeError: Não é possível ler a propriedade 'onAuthStateChanged' de undefined

No entanto, este post sugere um problema com a maneira como o firebase é importado no arquivo auth.

Eu o importei como: import Firebase from "../firebase";

Firebase é o nome da classe.

Os vídeos recomendados por Tristan são úteis, mas atualmente estou no episódio 9 e ainda não encontrei a parte que deveria ajudar a resolver esse problema. Alguém sabe onde encontrar isso?

NEXT ATTEMPT Em seguida - e tentando resolver apenas o problema de contexto - importei createContext e useContext e tentei usá-los conforme mostrado nesta documentação.

Não consigo passar um erro que diz:

Erro: chamada inválida. Ganchos só podem ser chamados dentro do corpo de um componente de função. Isso pode acontecer por um dos seguintes motivos: ...

Passei pelas sugestões neste link para tentar resolver esse problema e não consigo descobrir. Não tenho nenhum dos problemas mostrados neste guia de solução de problemas.

Atualmente - a declaração de contexto é a seguinte:

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

Passei um tempo usando este curso da udemy para tentar descobrir o contexto e o elemento de gancho para esse problema - depois de assistir - o único aspecto da solução proposta por Tristan abaixo é que o método createContext não é chamado corretamente em seu post. ele precisa ser "React.createContext", mas ainda não chega nem perto de resolver o problema.

Eu ainda estou preso.

Alguém pode ver o que deu errado aqui?

Mel
fonte
É indefinido porque você não está importando.
Josh Pittman
3
você só precisa adicionar exporta export const FirebaseContext?
Federkun
Oi Mel, desculpe-me pela resposta lenta. Eu tive duas semanas loucas de trabalho, então olhar para um computador à noite estava fora de questão! Atualizei minha resposta para que você forneça uma solução limpa e muito simples que possa ser verificada.
Tristan Trainer
Muito obrigado - vou dar uma olhada agora.
Mel
Olá, Mel, acabei de atualizar com a implementação correta das atualizações em tempo real do firestore (pode remover a parte onSnapshot para torná-la não em tempo real). Se esta é a solução, posso sugerir que você atualize sua pergunta para torná-la muito mais curto e conciso, para que outras pessoas que a visualizem possam encontrar a solução também, obrigado - desculpe novamente pela natureza lenta das respostas
Tristan Trainer

Respostas:

12

Edição principal : Demorei um pouco para analisar isso um pouco mais. É isso que eu vim com uma solução mais limpa. Alguém pode discordar de mim sobre o fato de ser uma boa maneira de abordar isso.

Gancho de autenticação da UseFirebase

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 }

Se authUser for nulo, não será autenticado; se o usuário tiver um valor, será autenticado.

firebaseConfigpode ser encontrado no console do firebase => Configurações do projeto => Aplicativos => Botão de opção Config

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

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

Esse gancho useEffect é o núcleo para rastrear as alterações de autenticação de um usuário. Adicionamos um ouvinte ao evento onAuthStateChanged de firebase.auth () que atualiza o valor de authUser. Esse método retorna um retorno de chamada para cancelar a inscrição deste ouvinte, que podemos usar para limpar o ouvinte quando o gancho useFirebase for atualizado.

Este é o único gancho necessário para a autenticação do firebase (outros ganchos podem ser feitos para o 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("[email protected]", "password0");
  }

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

Esta é uma implementação básica do Appcomponente de umcreate-react-app

Gancho do banco de dados useFirestore

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

Isso pode ser implementado no seu componente da seguinte maneira:

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

}

Isso remove a necessidade do React useContext à medida que obtemos atualizações diretamente do próprio firebase.

Observe algumas coisas:

  1. Salvar um documento inalterado não aciona um novo instantâneo, portanto, "salvar demais" não causa renderizações
  2. Ao chamar getDocument, o retorno de chamada onUpdate é chamado imediatamente com um "instantâneo" inicial, para que você não precise de código extra para obter o estado inicial do documento.

Editar removeu grande parte da resposta antiga

Tristan Trainer
fonte
11
obrigado por isso. Não vejo como você configurou isso. Com o provedor, recebo um erro que diz que createContext não está definido. Isso ocorre porque não há consumidor até agora. Onde você colocou o seu?
Mel
Ei, desculpe, createContext faz parte do react, então importe-o na parte superior como {createContext} de 'react'. Percebi que esqueci de mostrar para onde o provedor Firebase vai, vou editar a resposta
Tristan Trainer
Eu o importei no provedor, mas é indefinido. Eu acho que é porque não há consumidor para isso
Mel
11
O consumidor é o gancho useContext (), mas, olhando para sua pergunta novamente, parece que você não está exportando FirebaseContext do arquivo - é por isso que não consegue encontrar o contexto :)
Tristan Trainer
11
Olá @Mel, obrigado, isso é muito gentil, espero que seja útil no final. Hooks e Firebase estão bastante envolvidos e levei muito tempo para entender o problema e talvez ainda não tenha encontrado a melhor solução agora, posso criar um tutorial sobre minha abordagem em algum momento, pois é mais fácil de explicar à medida que você codifica isto.
Tristan Trainer
4

O Firebase é indefinido porque você não está importando. Primeiro, ele precisa ser firebase.firestore()como mostrado no exemplo nos documentos que você vinculou a https://github.com/CSFrequency/react-firebase-hooks/tree/master/firestore . Em seguida, é necessário importar o firebase no arquivo, import * as firebase from 'firebase';conforme descrito no leia-me do pacote do firebase https://www.npmjs.com/package/firebase

Josh Pittman
fonte
11
Estou importando no index.js
Mel
11
ReactDOM.render (<FirebaseContext.Provider value = {new Firebase ()}> <App /> </FirebaseContext.Provider>, document.getElementById ('root'));
Mel
11
É por isso que a abordagem funciona com componentDidMount
Mel
11
O withAuth HOC também é incluído no withFirebase.
Mel
3
Mas seu erro diz que é indefinido. Estou ajudando você a defini-lo e você não está me dizendo se a solução funciona ou qual é o erro resultante. Você sempre torna muito difícil ajudá-lo, Mel. O que quero dizer é que você está importando-o em um arquivo que não seja o arquivo do componente dashboard2, que é onde você o faz referência, o que está causando o erro. A inclusão de algo no índice não ajuda a sua compilação a entender o que está em um arquivo completamente diferente.
Josh Pittman em
2

EDIT (3 de março de 2020):

Vamos começar do zero.

  1. Eu criei um novo projeto:

    fio criar react-app firebase-hook-issue

  2. Excluí todos os 3 arquivos App * criados por padrão, removi a importação do index.js e também removi o service worker para ter um index.js limpo assim:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

const App = () => {
    return (
        <div>
            Hello Firebase!            
        </div>
    );
};

ReactDOM.render(<App />, document.getElementById('root'));
  1. Eu iniciei o aplicativo para ver o Hello Firebase! é impresso.
  2. Adicionei módulo firebase
yarn add firebase
  1. Eu executei o firebase init para configurar o firebase para esse projeto. Escolhi um dos meus projetos vazios do firebase e selecionei o Database e o Firestore, que acabam criando os próximos arquivos:
.firebaserc
database.rules.json
firebase.json
firestore.indexes.json
firestore.rules
  1. Eu adicionei importações para libs Firebase e também criou um Firebase classe e FirebaseContext . Por fim, envolvi o aplicativo no componente FirebaseContext.Provider e defini seu valor para uma nova instância do Firebase () . Nós teríamos apenas uma instância do aplicativo Firebase instanciada como deveríamos, porque deve ser um singleton:
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";

import app from "firebase/app";
import "firebase/database";
import "firebase/auth";
import "firebase/firestore";

class Firebase {
    constructor() {
        app.initializeApp(firebaseConfig);

        this.realtimedb = app.database();
        this.firestore = app.firestore();
    }
}

const FirebaseContext = React.createContext(null);

const firebaseConfig = {
    apiKey: "",
    authDomain: "",
    databaseURL: "",
    projectId: "",
    storageBucket: "",
    messagingSenderId: "",
    appId: "",
};

const App = () => {
    return <div>Hello Firebase!</div>;
};

ReactDOM.render(
    <FirebaseContext.Provider value={new Firebase()}>
        <App />
    </FirebaseContext.Provider>
    , document.getElementById("root"));
  1. Vamos verificar se podemos ler qualquer coisa do Firestore. Para verificar apenas a leitura primeiro, fui ao meu projeto no Firebase Console, abri meu banco de dados do Cloud Firestore e adicionei uma nova coleção chamada contadores com um documento simples contendo um campo chamado valor do tipo número e valor 0. insira a descrição da imagem aqui insira a descrição da imagem aqui

  2. Atualizei a classe App para usar o FirebaseContext que criamos, usei o hook Hook para o nosso contador simples e usei o hook useEffect para ler o valor do firestore:

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";

import app from "firebase/app";
import "firebase/database";
import "firebase/auth";
import "firebase/firestore";

const firebaseConfig = {
    apiKey: "",
    authDomain: "",
    databaseURL: "",
    projectId: "",
    storageBucket: "",
    messagingSenderId: "",
    appId: "",
};

class Firebase {
    constructor() {
        app.initializeApp(firebaseConfig);

        this.realtimedb = app.database();
        this.firestore = app.firestore();
    }
}

const FirebaseContext = React.createContext(null);

const App = () => {
    const firebase = React.useContext(FirebaseContext);
    const [counter, setCounter] = React.useState(-1);

    React.useEffect(() => {
        firebase.firestore.collection("counters").doc("simple").get().then(doc => {
            if(doc.exists) {
                const data = doc.data();
                setCounter(data.value);
            } else {
                console.log("No such document");
            }
        }).catch(e => console.error(e));
    }, []);

    return <div>Current counter value: {counter}</div>;
};

ReactDOM.render(
    <FirebaseContext.Provider value={new Firebase()}>
        <App />
    </FirebaseContext.Provider>
    , document.getElementById("root"));

Nota: Para manter a resposta o mais curta possível, verifiquei se você não precisa ser autenticado com o firebase, configurando o acesso ao firestore para o modo de teste (arquivo firestore.rules):

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // This rule allows anyone on the internet to view, edit, and delete
    // all data in your Firestore database. It is useful for getting
    // started, but it is configured to expire after 30 days because it
    // leaves your app open to attackers. At that time, all client
    // requests to your Firestore database will be denied.
    //
    // Make sure to write security rules for your app before that time, or else
    // your app will lose access to your Firestore database
    match /{document=**} {
      allow read, write: if request.time < timestamp.date(2020, 4, 8);
    }
  }
}

Minha resposta anterior: Você é bem-vindo a dar uma olhada no meu react-firebase-auth-skeleton:

https://github.com/PompolutZ/react-firebase-auth-skeleton

Segue principalmente o artigo:

https://www.robinwieruch.de/complete-firebase-authentication-react-tutorial

Mas um pouco reescrito para usar ganchos. Eu usei em pelo menos dois dos meus projetos.

Uso típico do meu projeto atual de estimação:

import React, { useState, useEffect, useContext } from "react";
import ButtonBase from "@material-ui/core/ButtonBase";
import Typography from "@material-ui/core/Typography";
import DeleteIcon from "@material-ui/icons/Delete";
import { FirebaseContext } from "../../../firebase";
import { useAuthUser } from "../../../components/Session";
import { makeStyles } from "@material-ui/core/styles";

const useStyles = makeStyles(theme => ({
    root: {
        flexGrow: 1,
        position: "relative",
        "&::-webkit-scrollbar-thumb": {
            width: "10px",
            height: "10px",
        },
    },

    itemsContainer: {
        position: "absolute",
        top: 0,
        left: 0,
        right: 0,
        bottom: 0,
        display: "flex",
        alignItems: "center",
        overflow: "auto",
    },
}));

export default function LethalHexesPile({
    roomId,
    tokens,
    onSelectedTokenChange,
}) {
    const classes = useStyles();
    const myself = useAuthUser();
    const firebase = useContext(FirebaseContext);
    const pointyTokenBaseWidth = 95;
    const [selectedToken, setSelectedToken] = useState(null);

    const handleTokenClick = token => () => {
        setSelectedToken(token);
        onSelectedTokenChange(token);
    };

    useEffect(() => {
        console.log("LethalHexesPile.OnUpdated", selectedToken);
    }, [selectedToken]);

    const handleRemoveFromBoard = token => e => {
        console.log("Request remove token", token);
        e.preventDefault();
        firebase.updateBoardProperty(roomId, `board.tokens.${token.id}`, {
            ...token,
            isOnBoard: false,
            left: 0,
            top: 0,
            onBoard: { x: -1, y: -1 },
        });
        firebase.addGenericMessage2(roomId, {
            author: "Katophrane",
            type: "INFO",
            subtype: "PLACEMENT",
            value: `${myself.username} removed lethal hex token from the board.`,
        });
    };

    return (
        <div className={classes.root}>
            <div className={classes.itemsContainer}>
                {tokens.map(token => (
                    <div
                        key={token.id}
                        style={{
                            marginRight: "1rem",
                            paddingTop: "1rem",
                            paddingLeft: "1rem",
                            filter:
                            selectedToken &&
                            selectedToken.id === token.id
                                ? "drop-shadow(0 0 10px magenta)"
                                : "",
                            transition: "all .175s ease-out",
                        }}
                        onClick={handleTokenClick(token)}
                    >
                        <div
                            style={{
                                width: pointyTokenBaseWidth * 0.7,
                                position: "relative",
                            }}
                        >
                            <img
                                src={`/assets/tokens/lethal.png`}
                                style={{ width: "100%" }}
                            />
                            {selectedToken && selectedToken.id === token.id && (
                                <ButtonBase
                                    style={{
                                        position: "absolute",
                                        bottom: "0%",
                                        right: "0%",
                                        backgroundColor: "red",
                                        color: "white",
                                        width: "2rem",
                                        height: "2rem",
                                        borderRadius: "1.5rem",
                                        boxSizing: "border-box",
                                        border: "2px solid white",
                                    }}
                                    onClick={handleRemoveFromBoard(token)}
                                >
                                    <DeleteIcon
                                        style={{
                                            width: "1rem",
                                            height: "1rem",
                                        }}
                                    />
                                </ButtonBase>
                            )}
                        </div>
                        <Typography>{`${token.id}`}</Typography>
                    </div>
                ))}
            </div>
        </div>
    );
}

Duas partes mais importantes aqui são: - gancho useAuthUser (), que fornece o usuário autenticado atual. - FirebaseContext que eu uso através do gancho useContext .

const firebase = useContext(FirebaseContext);

Quando você tem contexto para firebase, cabe a você implementar o objeto firebase ao seu gosto. Às vezes, escrevo algumas funções úteis, às vezes é mais fácil configurar os ouvintes diretamente no gancho useEffect que crio para o meu componente atual.

Uma das melhores partes desse artigo foi a criação do withAuthorization HOC, que permite especificar os pré-requisitos para acessar a página no próprio componente:

const condition = authUser => authUser && !!authUser.roles[ROLES.ADMIN];
export default withAuthorization(condition)(AdminPage);

Ou talvez até defina essas condições diretamente na implementação do seu roteador.

Espero que, ao examinar o repositório e o artigo, você tenha algumas boas idéias para aprimorar outras respostas brilhantes à sua pergunta.

fxdxpz
fonte
Comprei o livro dele e segui sua abordagem. Eu descobri que a abordagem das condições não funcionava realmente quando implementada e que o protocolo de autenticação estabelecido nesse livro falhou ao manter o status por meio de atualizações de componentes. Não encontrei uma maneira de usar o que está descrito nesse livro. De qualquer forma, obrigado por compartilhar seus pensamentos.
Mel
Eu não estou certo do que você quer dizer. Você já experimentou meu aplicativo esqueleto com seu projeto firebase? Todas as condições funcionam até onde eu sei, porque eu a uso em pelo menos três projetos.
fxdxpz 9/03