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.