React: how to use the useContext hook in combination with the useReducer and firebase for user authentification

Terchilă Marian
6 min readJan 16, 2022

This guide will address how the useReducer and the useContext react hooks can be used together to create a global state management system that feels more like using redux. We will use this tutorial to generate a real-world use case for our global state manager where we will implement firebase authentication to our react project.

There are a lot of things that could be said about redux but overall the community from what I’ve noticed so far is not as prone to use it now as it once was. The main reason for that is the enormous boilerplate required to first start with.

On the other hand, the Context API gained a lot of traction since now it is capable of producing global state management in combination with the useReducer hook that feels very much like using redux. As opposed to redux, this variant requires less friction and can fully accomplish the same purpose.

Let's get started

Setting up the project

To begin with, you can create a new react application or use your existing one. Feel free to skip this step in case you are not interested in setting up a new project with react and firebase

Next, we will have to install our dependency.

yarn add firebase

Once you’ve installed the dependency create a new firebase project in case you’d like to follow along with this example. After that enable the authentication with email and password from firebase.

After creating the firebase project, setting up the authentication provider create a web application in the settings menu and grab the API keys necessary to connect to the newly created project.

Create a new file named .env in the root directory of your project and update the contents of the following snippet with yours.

REACT_APP_API_KEY=
REACT_APP_AUTH_DOMAIN=
REACT_APP_DATABASE_URL=
REACT_APP_PROJECT_ID=
REACT_APP_STORAGE_BUCKET=
REACT_APP_MESSAGING_SENDER_ID=
REACT_APP_APP_ID=

We just set up the environment variables for the firebase configuration. To carry on what I like to do is to create a folder named configuration and a file within it named firebase.js where I initialise firebase and the other functionalities that I might be using throughout the project.

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

const firebaseConfig = {
apiKey: process.env.REACT_APP_API_KEY,
authDomain: process.env.REACT_APP_AUTH_DOMAIN,
databaseURL: process.env.REACT_APP_DATABASE_URL,
projectId: process.env.REACT_APP_PROJECT_ID,
storageBucket: process.env.REACT_APP_STORAGE_BUCKET,
messagingSenderId: process.env.REACT_APP_MESSAGING_SENDER_ID,
};

firebase.initializeApp(firebaseConfig);

export const auth = firebase.auth();
export const storage = firebase.storage();
export const firestore = firebase.firestore();

It should look something similar to this once you're done.

Now that we’re finally done setting up the environment we can address the state management.

The state management

According to the React documentation, the useContext hook:

const value = useContext(MyContext);

Accepts a context object (the value returned from React.createContext) and returns the current context value for that context. The current context value is determined by the value prop of the nearest <MyContext.Provider> above the calling component in the tree.

When the nearest <MyContext.Provider> above the component updates, this Hook will trigger a rerender with the latest context value passed to that MyContext provider. Even if an ancestor uses React.memo or shouldComponentUpdate, a rerender will still happen starting at the component itself using useContext.

Don’t forget that the argument to useContext must be the context object itself:

Correct: useContext(MyContext)

Incorrect: useContext(MyContext.Consumer)

Incorrect: useContext(MyContext.Provider)

Following along, the React documentation defines the useReducer hook:

const [state, dispatch] = useReducer(reducer, initialArg, init);

An alternative to useState. Accepts a reducer of type (state, action) => newState, and returns the current state paired with a dispatch method. (If you’re familiar with Redux, you already know how this works.)

useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one. useReducer also lets you optimize performance for components that trigger deep updates because you can pass dispatch down instead of callbacks.

To make an idea this is what our reducer is supposed to look like:

const authReducer = (state, action) => {
switch (action.type) {
case 'REGISTER':
return {...state, account: action.payload.account, profile: action.payload.profile}
case 'LOGIN':
return {...state, account: action.payload.account, profile: action.payload.profile}
case 'UPDATE_PROFILE':
return {...state, profile: action.payload.profile}
case 'UPDATE_ACCOUNT':
return {...state, account: action.payload.account}
case 'LOGOUT':
return {...state, account: {}, profile: {}}
case 'ERROR':
return {...state, errors: action.payload.errors}
default: {
throw new Error(`Unhandled action type: ${action.type}`)
}
}
}

As the react documentation states already, it is supposed to take in two parameters, the state and the action. In our case, the state will be the current state of the application and the action will contain the payload with the data to be updated.

For example, the REGISTER case will update the current state of the application with the account and the profile data that comes from the action.payload, everything else will stay the same as we’re using the spread operator for the state.

The default case ensures us that we will not run into weird behaviours and only accept the defined cases.

In order to use the Context API, we first need to define the initial state of the application.

const initialState = {
account: {},
profile: {},
errors: [],
}

This is how ours looks like, where the account represents the firebase account object and the profile is something I’ve come up with to ease the process of creating user profiles by using the firestore database.

Whenever an account is created we make a link in between the account and the firestore profile, this will help us later down the line in the event we want to store some other data about our users other than the bare minimum that the firebase account can hold.

const AuthContext = React.createContext(initialState)

This will create our context using the initial state prior defined.

To move forward what I like to do is to keep all my context files in a providers folder. For this example create a file name authProvider.js inside the providers folder.

const AuthProvider = ({children}) => {
const [state, dispatch] = React.useReducer(authReducer, initialState)
const value = {state, dispatch}


return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}

export {AuthContext, AuthProvider}

Here we define out AuthProvider and AuthContext.

The AuthProvider will return the provider for our context in a container style where we pass the children aka the components we want to have access to the auth state.

import React from 'react';
import ReactDOM from 'react-dom';
import {ModalProvider} from "./providers/modalProvider";
import App from "./App";
import {AuthProvider} from "./providers/authProvider";

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

This is how I like to organise my providers, where I wrap the application with all the providers I have inside the index.js file.

To use the state now all we have to do is to import it into our component

const {state, dispatch} = React.useContext(AuthContext)

Then dispatch the appropriate action along with the payload (if needed).

onClick={() => {
dispatch({type: 'LOGOUT'})
}

The final version should look something similar to this

import React from 'react'

/* REDUCER */
const
initialState = {
account: {},
profile: {},
errors: [],
}

const authReducer = (state, action) => {
switch (action.type) {
case 'REGISTER':
return {...state, account: action.payload.account, profile: action.payload.profile}
case 'LOGIN':
return {...state, account: action.payload.account, profile: action.payload.profile}
case 'UPDATE_PROFILE':
return {...state, profile: action.payload.profile}
case 'UPDATE_ACCOUNT':
return {...state, account: action.payload.account}
case 'LOGOUT':
return {...state, account: {}, profile: {}}
case 'ERROR':
return {...state, errors: action.payload.errors}
default: {
throw new Error(`Unhandled action type: ${action.type}`)
}
}
}

/* COMPONENT */
const
AuthContext = React.createContext(initialState)

const AuthProvider = ({children}) => {
const [state, dispatch] = React.useReducer(authReducer, initialState)
const value = {state, dispatch}

return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}

export {AuthContext, AuthProvider}

Congrats! If you followed along till this point you’re done and ready to go.

The full implementation of this guide can be found on my GitHub where I also define the functions for register, user autologin, update profile, update account, update email and so on.

https://github.com/Terkea/yunoTalks/blob/main/src/providers/authProvider.js

--

--