Integrating Firebase Authentication, Hooks, and Context into your ReactJS App

John Cassidy
6 min readJul 4, 2019

In this article you will learn how to:

  • Track a user in a functional component with useState
  • React in our application accordingly to changes in the user state
  • Subscribe to Firebase Authentication events and update our user state
  • Make our user state available via the Context API

Keeping track of our user with useState

Let’s start with a simple hook to initialize our user state for use in our application:

function App() {
const [user, setUser] = useState({ loggedIn: false });
if (!user.loggedIn) {
return <span>User is logged out</span>;
}
return <span>User is logged in</span>;
}

The result is a component that displays if we are logged in or not depending on the current state of user. Since we set our initial state to being logged out, this will always display that we are logged out.

Listening to Authentication Provider

The state of the user needs to come from an authentication provider, in this case basic firebase email authentication. We are going to setup a listener to changes in auth state in the firebase auth provider:

import firebase from 'firebase/app`;
import 'firebase/auth';
firebase.initializeApp(/* firebase config */);function onAuthStateChange() {
return firebase.auth().onAuthStateChanged(user => {
if (user) {
console.log("The user is logged in");
} else {
console.log("The user is not logged in");
}
});
}

This will keep a connection to the firebase authentication provider, and change whenever the backend state of the logged in user changes. Our next step will be to start listening to auth state changes when our application mounts, we can do this with a useEffect hook:

function App() {
const [user, setUser] = useState({ loggedIn: false });
useEffect(() => {
const unsubscribe = onAuthStateChange();
return () => {
unsubscribe();
};
}, []);


if (!user.loggedIn) {
return <span>User is logged out</span>;
}
return <span>User is logged in</span>;
}

and we can bring the data from the auth provider changes to our user state by connecting our setUser hook as a callback to our onAuthStateChange function:

function onAuthStateChange(callback) {
return firebase.auth().onAuthStateChanged(user => {
if (user) {
callback({loggedIn: true});
} else {
callback({loggedIn: false});
}
});
}
function App() {
const [user, setUser] = useState({ loggedIn: false });
useEffect(() => {
const unsubscribe = onAuthStateChange(setUser);
return () => {
unsubscribe();
};
}, []);

if (!user.loggedIn) {
return <span>User is logged out</span>;
}
return <span>User is logged in</span>;
}

By passing in setUser as a callback to our onAuthStateChange function, whenever the state of the user changes and onAuthStateChange is called, our user state will update accordingly.

Login and Logout Components

Because we are constantly listening to the state of the user in the backend and reacting to it, we can provide basic login / logout functionality to our app and allow our existing logic to continue working as expected.

Let’s provide some basic Components that we can use to represent a logged in user and a logged out user, and provide them with actions to perform (button to login and logout)

function LoginView({ onClick }) {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
return (
<div>
<input
onChange={event => {
setUsername(event.target.value);
}}
/>
<input
type="password"
onChange={event => {
setPassword(event.target.value);
}}
/>
<button
onClick={() => {
onClick(username, password);
}}
>
Login
</button>
</div>
);
}
function LogoutView({ onClick }) {
return (
<div>
<span>You are logged in</span>
<button onClick={onClick}>Logout</button>
</div>
);
}

If we place these within our app, and listen to action events from our new components, we start to see how it all comes together.

function App() {
const [user, setUser] = useState( {loggedIn: false});
useEffect(() => {
const unsubscribe = onAuthStateChange(setUser);
return () => {
unsubscribe();
}
}, []);
const requestLogin = useCallback((username, password) => {});
const requestLogout = useCallback(() => {});
if (!user.loggedIn) {
return <LoginView onClick={requestLogin} />;
}
return <LogoutView onClick={requestLogout} />;
}

Let’s expand our authentication functionality, and perform these actions with our callbacks.

function onAuthStateChange(callback) {
return firebase.auth().onAuthStateChanged(user => {
if (user) {
callback({loggedIn: true});
} else {
callback({loggedIn: false});
}
});
}
function login(username, password) {
firebase.auth().signInWithEmailAndPassword(username, password);
}
function logout() {
firebase.auth().signOut();
}
function App() {
const [user, setUser] = useState( {loggedIn: false} );
useEffect(() => {
const unsubscribe = onAuthStateChange(setUser);
return () => {
unsubscribe();
}
}, []);
const requestLogin = useCallback((username, password) => {
login(username, password);
});
const requestLogout = useCallback(() => {
logout();
}, []);
if (!user.loggedIn) {
return <LoginView onClick={requestLogin} />;
}
return <LogoutView onClick={requestLogout} />;
}

The above will now be a functioning application that correctly displays the LoginView Component when no user state is present, and a LogoutView Component when a user state is present.

Making the user available with Context

If we want to make our user state available to children of our root App functional component, we do have the option of passing it down as a prop on each child. However, this is cumbersome and prone to error. A better alternative is to use the Context API, where we wrap a high level component with a Provider, and allow any child within it to consume the provider data as a Consumer.

Expand the dataset provided in the user state with the email address of the logged in user:

function onAuthStateChange(callback) {
return firebase.auth().onAuthStateChanged(user => {
if (user) {
callback({ loggedIn: true, email: user.email });
} else {
callback({ loggedIn: false });
}
});
}

We start our work with sharing this new data by defining a basic Context:

const defaultUser = { loggedIn: false, email: "" };
const UserContext = React.createContext(defaultUser);
const UserProvider = UserContext.Provider;
const UserConsumer = UserContext.Consumer;

This will provide the tools needed to set a provider of data and a consumer of data. In our application, when a user is logged in, we want to make the user state available to any child component that cares about it. We can do this by first wrapping our root node (in this example, <Logout /> as a Provider:

function App() {
const [user, setUser] = useState( {loggedIn: false} );
useEffect(() => {
const unsubscribe = onAuthStateChange(setUser);
return () => {
unsubscribe();
}
}, []);
const requestLogin = useCallback((username, password) => {
login(username, password);
});
const requestLogout = useCallback(() => {
logout();
}, []);
if (!user.loggedIn) {
return <LoginView onClick={requestLogin} />;
}
return (
<UserProvider value={user}>
<LogoutView onClick={requestLogout} />
</UserProvider>

);
}

We can then make our <Logout /> Component a consumer of the UserContext by way of the useContext hook, and the user state will be available to it.

import React, { useContext } from 'react';function LogoutView({ onClick }) {
const user = useContext(UserContext);
return (
<div>
<span>You are logged in as {user.email}</span>
<button onClick={onClick}>Logout</button>
</div>
);
}

Handling errors and cleanup with callbacks

Our app currently assumes that login will be successful. For the situation in which it isn’t, and our authentication provider returns an error, we should handle this appropriately. This can be done by returning a Promise indicating the result of our login attempt, and then reacting to it. The following example demonstrates catching an error from the login function, and setting an error state that the application can then pass to the LoginView component.

function login(username, password) {
return new Promise((resolve, reject) => {
firebase
.auth()
.signInWithEmailAndPassword(username, password)
.then(() => resolve())
.catch(error => reject(error));
});
}
function App() {
const [user, setUser] = useState( {loggedIn: false} );
const [error, setError] = useState("");
useEffect(() => {
const unsubscribe = onAuthStateChange(setUser);
return () => {
unsubscribe();
}
}, []);
const requestLogin = useCallback((username, password) => {
login(username, password).catch(error => setError(error.code));
});
const requestLogout = useCallback(() => {
logout();
}, []);
if (!user.loggedIn) {
return <LoginView onClick={requestLogin} error={error}/>;
}
return (
<UserProvider value={user}>
<LogoutView onClick={requestLogout} />
</UserProvider>
);
}

Full code sample

The following code represents:

  • Creating a functional component that tracks user and error state
  • Reacting to changes in these states
  • Subscribing to Firebase Authentication provider for changes and updating out states accordingly
  • Using the Context API to make user data available through our application
import React, {
useCallback,
useContext,
useEffect,
useState
} from 'react';
import firebase from 'firebase/app';
import 'firebase/auth';
firebase.initialize({/* init data */});const defaultUser = { loggedIn: false, email: "" };const UserContext = React.createContext({});
const UserProvider = UserContext.Provider;
const UserConsumer = UserContext.Consumer;
function onAuthStateChange(callback) {
return firebase.auth().onAuthStateChanged(user => {
if (user) {
callback({loggedIn: true, email: user.email});
} else {
callback({loggedIn: false});
}
});
}
function login(username, password) {
return new Promise((resolve, reject) => {
firebase
.auth()
.signInWithEmailAndPassword(username, password)
.then(() => resolve())
.catch(error => reject(error));
});
}
function logout() {
firebase.auth().signOut();
}
function LoginView({ onClick, error }) {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
return (
<div>
<input
onChange={event => {
setUsername(event.target.value);
}}
/>
<input
type="password"
onChange={event => {
setPassword(event.target.value);
}}
/>
<button
onClick={() => {
onClick(username, password);
}}
>
Login
</button>
<span>{error}</span>
</div>
);
}
function LogoutView({ onClick }) {
const user = useContext(UserContext);
return (
<div>
<span>You are logged in as {user.email}</span>
<button onClick={onClick}>Logout</button>
</div>
);
}
function App() {
const [user, setUser] = useState( {loggedIn: false} );
const [error, setError] = useState("");
useEffect(() => {
const unsubscribe = onAuthStateChange(setUser);
return () => {
unsubscribe();
}
}, []);
const requestLogin = useCallback((username, password) => {
login(username, password).catch(error => setError(error.code));
});
const requestLogout = useCallback(() => {
logout();
}, []);
if (!user.loggedIn) {
return <LoginView onClick={requestLogin} error={error}/>;
}
return (
<UserProvider value={user}>
<LogoutView onClick={requestLogout} />
</UserProvider>
);
}

--

--