Skip to the main content

Authentication Context

Great way to structure authentication context in React apps.

––– views

This structure of context is adapted from Kent C Dodds Journal Post.

JavaScript Version

import axios from 'axios';
import { createContext, useContext, useEffect, useReducer } from 'react';
 
const StateContext = createContext({
  authenticated: false,
  user: null,
  loading: true,
});
 
const DispatchContext = createContext(null);
 
const reducer = (state, { type, payload }) => {
  switch (type) {
    case 'LOGIN':
      return {
        ...state,
        authenticated: true,
        user: payload,
      };
    case 'LOGOUT':
      localStorage.removeItem('token');
      return {
        ...state,
        authenticated: false,
        user: null,
      };
    case 'POPULATE':
      return {
        ...state,
        user: {
          ...state.user,
          ...payload,
        },
      };
    case 'STOP_LOADING':
      return {
        ...state,
        loading: false,
      };
    default:
      throw new Error(`Unknown action type: ${type}`);
  }
};
 
export const AuthProvider = ({ children }) => {
  const [state, defaultDispatch] = useReducer(reducer, {
    user: null,
    authenticated: false,
    loading: true,
  });
 
  const dispatch = (type, payload) => defaultDispatch({ type, payload });
 
  useEffect(() => {
    const loadUser = async () => {
      try {
        const token = localStorage.getItem('token');
        if (token === null || token === undefined) {
          return;
        }
        const res = await axios.get('/profile', {
          headers: {
            'Content-Type': 'application/json',
            Authorization: `Bearer ${token}`,
          },
        });
        dispatch('LOGIN', res.data.data);
      } catch (err) {
        console.log(err);
        localStorage.removeItem('token');
      } finally {
        dispatch('STOP_LOADING');
      }
    };
 
    loadUser();
    // eslint-disable-next-line
  }, []);
 
  return (
    <StateContext.Provider value={state}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </StateContext.Provider>
  );
};
 
export const useAuthState = () => useContext(StateContext);
export const useAuthDispatch = () => useContext(DispatchContext);

Usage

You can wrap your code with <AuthProvider> in App.jsx in React or _app.jsx in Next.js

Then, to use the state and dispatch, we can use these 2 hooks.

const { authenticated, user } = useAuthState();
const dispatch = useAuthDispatch();

With this context, you can also implement loading, usually in PrivateRoute component

// components/PrivateRoute.jsx
import { ImSpinner9 } from 'react-icons/im';
import { Route, Redirect } from 'react-router-dom';
import { useAuthState } from '../contexts/AuthContext';
 
const PrivateRoute = (props) => {
  const { authenticated, loading } = useAuthState();
 
  if (loading) {
    return (
      <div className='bg-primary mt-20 flex min-h-screen flex-col items-center justify-center'>
        <ImSpinner9 className='mb-2 animate-spin text-4xl text-yellow-400' />
        <p>Loading...</p>
      </div>
    );
  }
 
  return authenticated ? <Route {...props} /> : <Redirect to='/login' />;
};
export default PrivateRoute;

TypeScript Version

import axios from 'axios';
import React, { createContext, useContext, useEffect, useReducer } from 'react';
 
type User = {
  email: string;
  name: string;
} | null;
type AuthState = {
  authenticated: boolean;
  user: User;
  loading: boolean;
};
type Action =
  | { type: 'LOGIN'; payload: User }
  | { type: 'POPULATE'; payload: User }
  | { type: 'LOGOUT' }
  | { type: 'STOP_LOADING' };
type Dispatch = React.Dispatch<Action>;
 
const StateContext = createContext<AuthState>({
  authenticated: false,
  user: null,
  loading: true,
});
const DispatchContext = createContext(null);
 
const reducer = (state: AuthState, action: Action) => {
  switch (action.type) {
    case 'LOGIN':
      return {
        ...state,
        authenticated: true,
        user: action.payload,
      };
    case 'LOGOUT':
      localStorage.removeItem('token');
      return {
        ...state,
        authenticated: false,
        user: null,
      };
    case 'POPULATE':
      return {
        ...state,
        user: {
          ...state.user,
          ...action.payload,
        },
      };
    case 'STOP_LOADING':
      return {
        ...state,
        loading: false,
      };
    default:
      throw new Error('Unknown action type');
  }
};
 
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
  const [state, dispatch] = useReducer(reducer, {
    user: null,
    authenticated: false,
    loading: true,
  });
 
  useEffect(() => {
    const loadUser = async () => {
      try {
        const token = localStorage.getItem('token');
        if (token === null || token === undefined) {
          return;
        }
        const res = await axios.get('/profile', {
          headers: {
            'Content-Type': 'application/json',
            Authorization: `Bearer ${token}`,
          },
        });
        dispatch({ type: 'LOGIN', payload: user });
      } catch (err) {
        // eslint-disable-next-line no-console
        console.log(err);
        localStorage.removeItem('token');
      } finally {
        dispatch({ type: 'STOP_LOADING' });
      }
    };
 
    loadUser();
    // eslint-disable-next-line
  }, []);
 
  return (
    <StateContext.Provider value={state}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </StateContext.Provider>
  );
};
 
export const useAuthState = () => useContext(StateContext);
export const useAuthDispatch: () => Dispatch = () =>
  useContext(DispatchContext);

Additional Resources

View more authentication pattern for Next.js to avoid flashing by reading this journal.

For Typescript, refer to this demo site.