Custom hook for firebase user management

By May 15, 2019javascript, react

Photo by Timon Wanner on Unsplash

Rather than using Redux or similar to manage global state (e.g. whether the user is authenticated or not), since React 16.8 you can now use hooks and context. Below is a simple example using create-react-app.

Create a new app

> create-react-app firebase-hook

Add firebase and reach router from NPM

npm install --save firebase @reach/router

Assuming you’ve created a new project on firebase (https://console.firebase.google.com/) and, in setting, added a app for ‘web’, copy the config data and store it in a firebase-config.js file. It should look something like this.

// firebase-config.js

const config = {
  apiKey: "ahsdflkhasfl;khadlfkhasd;lkfhals;dkf",
  authDomain: "<projectId>.firebaseapp.com",
  databaseURL: "https://<projectId>.firebaseio.com",
  projectId: "<projectId>",
  storageBucket: "<projectId>.appspot.com",
  messagingSenderId: "097093274093274",
  appId: "o090237409327409732"

export default config;

Create a ‘db.js’ file under ‘./src’. This is where we will initialise firebase.

// db.js

import * as firebase from 'firebase/app'
import 'firebase/firestore';
import config from './firebase-config;

// initialise firebase
firebase.initialiseApp(config);

// export firestore
export const db = firebase.firestore();

Create a ‘contexts’ directory in your project ( I put mine under ./src) and create a userContext.js file in it. We’ll use this file to store the context of the user, if there is one logged in, and pass it to other components using a customer hook.

// userContext.js

import { createContext } from 'react';

const userContext = createContext({
  user: null
)}

Create a ‘hooks directory in your project (I put mine under ‘./src’) and create a useAuth.js file in it. This will be our ‘auth’ hook through which we can access the user status in components.

In summary, what this hook does is:

  • store the state of the user (e.g. are they logged in) and initialise this by getting the current logged in user using firebase if there is one ([state, setState]);
  • use an effect to
    • listen for a change in the authentication state of the firebase user (e.g. did they log out, has their login timed out, etc);
    • update the stored user state is the user authentication state changes; and
    • stop listening for firebase authentication state changes when the component unmount (like when the app is closed)
  • export the useAuth hook as the default export.
// useAuth.js
// Custom hook for tracking user details (e.g. are they authenticated)

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

// custom hook
const useAuth = () => {

  // initialise a state object to sore the firebase logged in use data
  const [state, setState] = useState( () => {
    
    const user = firebase.auth.currentUser;
    return { 
      initialising: !user,
      user: user,
    };
  });

  // update the state when the firebase user authentication state changes
  const on Change = (user) => {
    setState({ initialising: false, user: user });
  }

  // use an effect to:
  //  - componentDidMont => add a watcher to firebase auth
  //  - componentDidUnmount => unsubscribe from the watcher
  userEffect ( () => {
   
    // watcher to listen to auth state change
    const unsubscribe = firebase.auth().onAuthStateChanged(onChange);
    
    // unsubscribe to the watcher when the component unmount
    return () => unsubscribe();
  }, [])

  return state;

}

export default useAuth;

To enable use to login to and interact with firebase, and firestore, we need to set up some functions to interact with the firebase API.

Create a new directory under ‘./src’ called ‘auth’ and create a new file in it call ‘auth.js’

In this file we’ll implement functions to

  • create a user account on firebase using an email and password;
  • login to firebase using an email and password; and
  • logout of firebase.

For email login to work, you’ll need to enable it in your firebase project ( https://console.firebase.google.com/<prijectId>/authentication/providers )

Note that firebase provides a lot of login options but we’ll only implement the email one.

// auth.js

import * as firebase from 'firebase/app';
import 'firebase/auth';
import { useContext } from 'react';
import userContext from '../contexts/userContext';

// facilitate creation of a new account
export const createUserWithEmail = async (email, password) => {
  try {
    await firebase.auth().createUserWithEmailAndPassword(email, password);
  } catch (err) {
    console.log(err);
    throw err;
  }
}

// facilitate login to firebase with email and password
export const loginWithEmail = async ( email, password ) => {
  try {
    await firebase.auth().signInWithEmailAndPassword( email, password     );
  } catch (err) {
    console.log(err);
    throw err;
  }
}

//enable signet
export const signOut = () => {
  firebase.auth().signOut();
}

We initialise the user and pass their details into our userContext provider in App.js to ensure that only one firebase context is created.

// App.js

import React from 'react';
import { Router } from '@reach/router'
import TestComponent from './components/Test';
import Home from './components/Home';
import Login from './components/login';
...
...
import useAuth from './hooks/useAuth';
import userContext from './contexts/usrContext';

function App() {

  const { initialising, user } = useAuth();

  if ( initialising ) return <div> Loading </>

  return (
    <div>
      <userContext.Provider value = ({ user: user )}>
      
        <Router>

          <Home path="/"/>
          <Login path = "/login">

        </Router>
      
     </userContext.Provider>
    </div>
  );
};

export default App;

You’ll notice we imported a ‘Home’ and ‘Login’ component in App.js, and we are using reach router to route to these pages.

Login is a simple login form to enable us to login to firebase.

In this component we

  • use state to track the user’s email and password for either the creation of a new account or to log in;
  • provide a form for the user to enter their email and password and two buttons, one to create a new account and one to log into an existing one
// Login.js

import React, { useState } from 'react';
import { loginWithEmail, createUserWithEmail } from '../auth/auth';
import { Form, FormGroup, Label, Input, Button, Container, Col, Row } from 'reactstrap';
import styles from './Login.module.css';
import { navigate } from '@reach/router';

const Login = (props) => {

    const [creds, setCreds] = useState({
        email: '',
        password: ''
    });

    const [status, setStatus] = useState('Nothing to report');

    const onChange = (event) => {

        setCreds({
            ...creds,
            [event.target.name]: event.target.value
        });

    }

    // login 
    const handleLogin = async (event) => {
        event.preventDefault();

        try {

            await loginWithEmail(creds.email, creds.password);
            setStatus('Logged in')
            navigate('/');

        } catch (err) {
            setStatus(err);
        }
    }

    // Sign up 
    const handleSignup = async (event) => {
        event.preventDefault();


        try {
            await createUserWithEmail(creds.email, creds.password);
            setStatus('Signed up')

        } catch (err) {
            console.log(err);
            setStatus(err);
        }
    }

  return (

    <>
      <form>
        <p>Email</p>
        <input 
          name='email'
          type='text' 
          placeholder='email'
          value = {creds.email}
          onChange = {onChange}/>
        <p>Password</p>
        <input 
          name='password'
          type='password' 
          placeholder='password'
          value = {creds.password}
          onChange = {onChange}/>
      <button onClick={handleSignnp}>Signup</button>
      <button onClick={handleLogin}>Login</button>
      </form>
    </>
  )
};

export default Login;

Home is a simple home page component from which we will be able to login, logout and also see out user state.

If the user is logged in we’ll see their firebase ‘uid’, other wise we’ll see ‘Hello world’.

import React, { useContext, useState } from 'react';
import userContext from '../contexts/userContext';
import { Link } from '@reach/router';
import { signOut } from '../../auth/auth';


const Home = (props) => {


    const [currentUser, setCurrentUser] = useState(null);

    const { user } = useContext(userContext)

    return (
        <Link to='/login'> Login </Link>
        <Link to='/' onClick={signOut}> Logout </>
        <div>Hello {user ? user.uid : 'world'}</div>
    )

}
export default Home;

To initialise the firebase connection and start our app, we simply import the ‘db.js’ file we created earlier, and App.js, into index.js

// index.js
import React from 'react';
import ReactDOM from 'react-doe';
import App from './App;
import './db';

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

That’s it a simple firebase auth example.