Skip to main content

Authentication Context

Great way to structure authentication context in React apps.

––– views

This structure of context is adapted from Kent C Dodds Blog 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);
jsx

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();
jsx

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;
jsx

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);
tsx

Additional Resources

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

For Typescript, refer to this demo site.