Thursday, May 28, 2020

Using React Hooks with Firebase Authentication


If you are a React developer, and unless you have been living under a rock for the past year, you will be familiar with Context, Reducers, and Hooks. Sure, these are usually just called "Hooks" or "custom hooks". Hooks give the developer two advantages over the old-school method of class-based components. These are:

  • Ability to build an entire application using function components, and
  • Elimination of the need for Redux.

Functional components make the code more concise and readable. They also provide a fairly significant performance boost over classes. As for Redux. Well. Good riddance.

Armed with hooks I decided that my next React project would be written entirely with functions and hooks. And this approach worked great until I needed to wire up Firebase Authentication. I discovered that the tutorials available provided examples using class-based components. This article then, is my attempt to fill that gap with my notes on using Hooks with Firebase Authentication.

Here are the things I had to build.

  • A "context" with a "context provider",
  • A "reducer",
  • A custom hook that uses an Effect hook,
  • Helper functions to interact with Firebase, and
  • All the JSX to make it work.

The Reducer

Let's start by writing a Reducer. That might seem a bit backward as I typically begin with my Context. But since the Reducer is referenced by the Context, I'll start with the Reducer. The reducer is used to make changes to the state of the properties managed by the Context. If all this sounds like jargon (which it is), then I suggest reading up on React Hooks; especially useContext, useReducer, and useEffect. I'd love to explain all that here, but well, this article will be very long as it is.

The Reducer and the Context will be supporting Firebase Authentication. I should mention here that I am using the Authentication SDK and not the drop-in UI. I should also mention that I am only describing email based authentication. The properties of a "user" in Firebase are:

  • Name, which is the "display name" of the user. The display name is usually the user's full name.
  • Email, an email address the user is treating as her id.
  • PhotoURL, a URL to an image the user has uploaded for their avatar.
  • EmailVerified, a boolean value that indicates the user responded to a verification message.
  • Uid, which is a global unique identifier assigned by Firebase to the user.

Reducers contain logic for adding, removing, or manipulating the Context's data. In this case though, the user data is managed by Firebase via its' SDK. All our reducer needs to do is assure that the context is current. I created a file named SessionReducer.js and included the code below.

    export const SessionReducer = (state, action) => {
        switch (action.type) {
          case "UPDATE":
            return {
              name: action.session.name,
              email: action.session.email,
              photourl: action.session.photourl,
              emailVerified: action.session.emailVerified,
              uid: action.session.uid
            };
          default:
            return state;
        }
      };

The action.type property is part of the Reducer specification. So the Reducer is passed the state and an action object and returns the new value of the state. In this case, we will only take one action that I have set to "UPDATE". The value UPDATE, by-the-way, is a discresionary name set by the developer. Also note that I do not need any import statements here.

The Context

The Context is a bit bigger and contains a couple of functions. I'll show the code and then describe it. I created a file named SessionContext.js and included the code below.

    // React imports
    import React, { createContext, useReducer, useContext, useEffect } from "react";
    
    // Firebase imports
    import firebase from "../firebase";
    
    // My imports
    import { SessionReducer } from "../reducers/SessionReducer";
    
    // initial state values
    const initialState = {
      name: null,
      email: null,
      photourl: null,
      emailVerified: false,
      uid: null
    };
    
    // create the context
    export const SessionContext = createContext();
    
    // create the context provider
    const SessionContextProvider = props => {
      const [session, dispatch] = useReducer(SessionReducer, initialState);
      return (
        
          {props.children}
        
      );
    };
    
    // create the custom hook
    export const useSession = () => {
      const contextState = useContext(SessionContext);
      const { dispatch } = contextState;
      useEffect(() => {
        firebase.auth().onAuthStateChanged(user => {
          var currentUser = {};
          if (user) {
            currentUser = {
              name: user.displayName,
              email: user.email,
              photourl: user.photoURL,
              emailVerified: user.emailVerified,
              uid: user.uid
            };
          } else {
            currentUser = initialState;
          }
          dispatch({
            type: "UPDATE",
            session: currentUser
          });
        });
      }, [dispatch]);
      return contextState;
    };
    
    export default SessionContextProvider;    

Notice near the top of the code I have included an import of the Reducer shown earlier. I have not described the structure of my application but suffice to say that my Contexts and Reducers each reside in folders made for that purpose. You can organize your source files any way you like.

Following the imports, I create and export the SessionContext using React's API for that purpose. It's one line of code that requires no parameters. Next I have a few lines of code to create the "Context Provider". This is a small amount of code that includes JSX that ties the context to components in the application. React has two methods for this: Provider and Consumer, but since I am using functional components exclusively I must use the Provider method.

The Provider binds the Reducer to the Context. In this code I use the useReducer function to deconstruct its' return value into the "session" and the "dispatch". The session is the current state and the dispatch is the function I supplied when I wrote the Reducer. These values are passed as props (via HTML attributes) to the provider context. The {props.children} value assures that this context provider will be available to all children components to the component where it is used. I should also mention that the provider context is just another component. In my case, the context provider is my default exported function.

The Custom Hook

The last function creates the custom hook. By convention the names of hooks start with "use" in lower case; i.e. useContext, useReducer, or in my case useSession. The custom hook first retrieves data from this context, which is the state and the dispatch function, and stores it in contextState. The dispatch function is deconstructed out of the contextState. For my purpose here, I will not need the actual values of the state.

The work of this custom hook is accomplished within a useEffect function. If you’re familiar with React class lifecycle methods, you can think of useEffect Hook as componentDidMount, componentDidUpdate , and componentWillUnmount combined. In this case, whenever a component that uses our hook is rendered, a call is made to the Firebase onAuthStateChanged method. That method is passed a function that checks the state of the current Firebase user. If the user exists then the Firebase used is passed to the dispatch function, otherwise the initial state of the user (which is null) is passed to dispatch.

Wiring it up

The rest is simply writing the custom hook into the app. In this case I wanted to make the hook available to the entire application. To accomplish this I added my context provider to the app.js main code file.

    // React imports
    import React from "react";
    import { BrowserRouter, Switch, Route } from "react-router-dom";
    
    // Material UI imports
    import { MuiThemeProvider, useTheme } from "@material-ui/core/styles";
    import CssBaseline from "@material-ui/core/CssBaseline";
    import Box from "@material-ui/core/Box";
    import Container from "@material-ui/core/Container";
    
    // My imports
    import Dashboard from "./components/dashboard/Dashboard";
    import AppHeader from "./components/layout/AppHeader";
    import HomeDetail from "./components/houses/HomeDetail";
    import SignIn from "./components/auth/SignIn";
    import SignUp from "./components/auth/SignUp";
    import HouseContextProvider from "./contexts/HouseContext";
    import SessionContextProvider from "./contexts/SessionContext";
    import LaunchPage from "./components/dashboard/LaunchPage";
    
    const App = () => {
      const theme = useTheme();
    
      return (
        <BrowserRouter>
          <SessionContextProvider>
            <MuiThemeProvider theme={theme}>
              <HouseContextProvider>
                <CssBaseline />
                <AppHeader />
                <Container maxWidth="xl">
                  <Box m={3}>
                    <Switch>
                      <Route path="/" exact component={LaunchPage} />
                      <Route path="/dashboard" component={Dashboard} />
                      <Route path="/homes/:id" component={HomeDetail} />
                      <Route path="/signin" component={SignIn} />
                      <Route path="/signup" component={SignUp} />
                    </Switch>
                  </Box>
                </Container>
              </HouseContextProvider>
            </MuiThemeProvider>
          </SessionContextProvider>
        </BrowserRouter>
      );
    };
    
    export default App;

In this file there are just a couple of lines to point out. First is the import of the SessionContextProvider. And the others are the JSX SessionContextProvider tags.

To signup a new user bind the function below to your form submission handler.

...
import { signup } from "../../services/firebaseAuth";
...
// form actions
const handleSubmit = e => {
  e.preventDefault();
  enroll();
};

// firebase signup function
const enroll = () => {
  const results = validate(
    {
      email: email,
      password: password,
      firstName: firstName,
      lastName: lastName,
      confirmedPwd: confirmedPwd
    },
    {
      email: constraints.email,
      password: constraints.password,
      firstName: constraints.firstName,
      lastName: constraints.lastName,
      confirmedPwd: constraints.confirmedPwd
    }
  );

  if (results) {
    setErrors(results);
  } else {
    setErrors(null);
    signup(email, password);
  }
};

A couple of points about the code above. First, the validate function it its' constraints are out of the scope of this article. If anyone reads this, and someone asks about validate then I will consider writing another post to describe that function. And second, there is a helper function signup that I show a bit later.

Signing in is similar.

...
import { SessionContext, useSession } from "../../contexts/SessionContext";
import { signin } from "../../services/firebaseAuth";
    
const SignIn = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [errors, setErrors] = useState();
const { session } = useSession(SessionContext);
const handleSubmit = e => {
  e.preventDefault();
  authenticate();
};

// firebase signin function
const authenticate = () => {
const results = validate(
    {
      email: email,
      password: password
    },
    {
      email: constraints.email,
      password: constraints.password
    }
  );
 
  if (results) {
    setErrors(results);
  } else {
    setErrors(null);
    signin(email, password);
  }
};
...

Again, the signin function is shown later. And lastly I sign out with a link button on my AppBar that executes the signout fuction (and yes, the function must be imported).

    ...
    <button color="inherit" component="{Link}" onclick="{signout}" to="/">
        Sign Out
    </button>
    ...

Firebase

The final pieces of the puzzle are the supporting functions that call out to Firebase. These are the signup, signin, and signout functions mentioned above. I organized the functions into a single source code file. For purposes of this paper, these functions were copied directly from the Firebase documentation website and do not incude any of my application specific code. I created the file firebaseAuth.js for this. The code is below.

    // Firebase imports
    import firebase from "../firebase";
    
    export const signup = (email, password) => {
      firebase
        .auth()
        .createUserWithEmailAndPassword(email, password)
        .catch(error => {
          // Handle Errors here.
          alert("Error during sign up " + error.message); // delete this!
          var errorCode = error.code;
          var errorMessage = error.message;
          if (errorCode === "auth/weak-password") {
            alert("The password is too weak.");
          } else {
            alert(errorMessage);
          }
          console.log(error);
        });
    };
    
    export const signin = (email, password) => {
      firebase
        .auth()
        .signInWithEmailAndPassword(email, password)
        .catch(function(error) {
          // Handle Errors here.
          var errorCode = error.code;
          var errorMessage = error.message;
          if (errorCode === "auth/wrong-password") {
            alert("Wrong password.");
          } else {
            alert(errorMessage);
          }
          console.log(error);
        });
    };
    
    export const signout = () => {
      firebase
        .auth()
        .signOut()
        .then(function() {
          // Sign-out successful.
        })
        .catch(function(error) {
          console.log(error);
        });
    };

So the next person who needs to implement Firebase Authentication with React Hooks now has some reference material to start with. Questions and comments are welcome.

No comments:

Post a Comment

You might also like ...