This code snippet is all you need to manage auth state on your react application uses the Context API to manage user state across the application.
no more babbling, let's just dive into it.
Imports and Type Definitions
import { createContext, Dispatch, ReactNode, SetStateAction, useContext, useEffect } from "react";
import { useLocalStorage } from "../utils/useLocalStorage";
type SignInForm = { email: string; password: string; };
type User = { id: number; email: string; };
type AuthState = User & { exp: number };
type UserContextType = {
user: User | null;
setUser: Dispatch<SetStateAction<AuthState | null>>;
signOut: () => Promise<string | undefined>;
signIn: (signInForm: SignInForm) => Promise<string | undefined>;
};
We start by importing necessary React hooks and a custom useLocalStorage
hook. Then, we define TypeScript types for our authentication system, including SignInForm
, User
, AuthState
, and UserContextType
.
Creating the Context and Custom Hook
const AuthDataContext = createContext<UserContextType | undefined>(undefined);
export const useAuth = (): UserContextType => {
const context = useContext(AuthDataContext);
if (!context) {
throw new Error("useAuth must be used within a UserDataProvider");
}
return context;
};
Here, we create the AuthDataContext
and a custom useAuth
hook. This hook ensures that we're using the context within a provider and provides a convenient way to access our auth state.
AuthProvider Component
export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [user, setUser] = useLocalStorage<AuthState | null>("user", null);
// ... (other functions)
return (
<AuthDataContext.Provider value={{ user, setUser, signIn, signOut }}>
{children}
</AuthDataContext.Provider>
);
};
The AuthProvider
component is the core of our auth system. It uses the useLocalStorage
hook to persist the user state and provides the context value to its children.
JWT Expiration Check
const isJwtExpired = (unixTime: number) => {
const currentTime = Math.floor(Date.now() / 1000);
const timeRemaining = unixTime - currentTime;
if (timeRemaining <= 0) {
console.log("The JWT is expired.");
setUser(null);
return true;
} else {
const hours = Math.floor(timeRemaining / 3600);
const minutes = Math.floor((timeRemaining % 3600) / 60);
console.log(`Time remaining before JWT expires: ${hours} hours ${minutes} minutes`);
return false;
}
};
This function checks if the JWT has expired and logs the remaining time if it's still valid.
Sign Out Function
const signOut = async () => {
const res = await fetch("http://localhost:8080/auth/signout", { method: "POST" });
setUser(null);
if (!res.ok) {
console.log("Error signing out");
return (await res.text()) || "Something went wrong";
}
};
The signOut
function makes a POST request to the signout endpoint and clears the user state.
Sign In Function
const signIn = async (signInForm: SignInForm) => {
const res = await fetch("http://localhost:8080/auth/signin", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(signInForm),
});
if (!res.ok) {
return (await res.text()) || "Something went wrong";
}
const data = (await res.json()) as { user: User; exp: number };
if (data) {
setUser({ ...data.user, exp: data.exp });
}
};
The signIn
function sends the user's credentials to the signin endpoint and updates the user state with the response data.
useEffect for JWT Expiration Check
useEffect(() => {
if (!user) return;
if (isJwtExpired(user.exp)) signOut();
}, [user]);
This effect runs whenever the user state changes, checking if the JWT has expired and signing out if necessary.
Here's an example implementation of the useLocalStorage
hook btw
import { useState, useEffect, Dispatch, SetStateAction } from "react";
export function useLocalStorage<T>(
key: string,
initialValue: T
): [T, Dispatch<SetStateAction<T>>] {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.log(error);
return initialValue;
}
});
const setValue: Dispatch<SetStateAction<T>> = (value) => {
try {
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.log(error);
}
};
useEffect(() => {
const handleStorageChange = (event: StorageEvent) => {
if (event.key === key) {
setStoredValue(JSON.parse(event.newValue || "null"));
}
};
window.addEventListener("storage", handleStorageChange);
return () => window.removeEventListener("storage", handleStorageChange);
}, [key]);
return [storedValue, setValue];
}
and you're done? easy peasy lemon squeezy.. make sure to modify the fetching logic for your own api structure if needed.