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.