Omniscient

React Context: A Basic Example for Authentication

Published: Last updated:

In this post I want to introduce a nice and simple use-case for React Context that I've used in my project Cobalt (demo: https://cobalt.joshrowe.dev/).

Motivation

Certain types of data may need to be used throughout the whole application and at any level of the component hierarchy. In order to avoid "prop drilling", in which each component must pass down the data as a prop to the child below it in order to get it to the target component that needs said data, we can make that data globally available.

Let's explore how to do that using React Context and a custom hook to wrap it by means of an example: authentication data and login/logout functionality.

Implementation

  1. Set up our types and create the Context data.

The context defines what data will be available from anywhere (below the provider) throughout the component tree.

import { createContext, Dispatch, SetStateAction, useContext, useEffect, useState } from 'react'
import authService from '../services/auth.service'

export type User = {
  username: string;
  id: string;
  is_admin: boolean;
}

interface AuthContext {
  user?: User;
  setUser: Dispatch<SetStateAction<User | undefined>>
}

const AuthContext = createContext<AuthContext>({} as AuthContext)
  1. Create the Provider.

The Provider is what "provides" the data - meaning that any component that is below the Provider can access that context data. This is important because it means that you can have multiple providers in a single application. useContext will take data from the closest parent Provider component making it also possible to nest providers.

const AuthProvider = ({ children }: any) => {
  const [user, setUser] = useState<User | undefined>(undefined)

  // Attempt a login on mount so when we refresh page we can check that we're logged in
  useEffect(() => {
    authService
      .checkLoggedIn()
      .then(setUser)
      .catch(() => setUser(undefined))
  }, [])

  return <AuthContext.Provider value={{ user, setUser }}>
    {children}
  </AuthContext.Provider>
}
  1. Finally, we can define our custom hook!
const useAuth = () => {
  const { user, setUser } = useContext(AuthContext)

  const login = async (username: string, password: string) => {
    const user = await authService.login(username, password)
    setUser({ username: user.username, id: user.user_id, is_admin: user.is_admin })
  }

  const logout = async () => {
    await authService.logout()
    setUser(undefined)
  }

  return { user, login, logout }
}

export { AuthProvider, useAuth }

From the hook we export two convenience functions login and logout that internally use setUser but abstract that from the consumer of the hook.

Now any components that needs to either display information based on whether a user is logged in or not, or perform login/logout functionality like on a login form or signout button, can be accessed by using this custom hook like so:

const LoginButton = () => {
  const { user } = useAuth()
  
  return user // remember user is either User or undefined
    ? (<span>Logged in as {user.username}</span>)
    : (<button onClick={() => openLoginForm()}>Login</button>)
}

On a login form you can destructure the login function to submit the form.

Conclusion

Using Context with a custom hook allows us to make a nice clean API for internal code to re-use the authentication data and logic without having to do prop drilling or expose setState itself. We expose only the data and functions that are relevant in an easy to consume const { ... } = useAuth() call.

N.B. In a production application we would add more error handling to the login and logout async functions but I have forgone doing so in this tutorial for the purposes of brevity and simplicity.

The full source code is available here hooks/use-auth.tsx


Thank you for reading,
Omniscient