How to create an auth hook to work with zustand and protect your React Router SPA
Tuesday, September 09, 2025One of the most common problems when working with a React single-page application is knowing how to protect your routes.
Today, I want to share a very useful Zustand hook that can be very useful for this problem.
Assumptions and how it works
The front end will log in to a backend that will return a JSON web token, which will then be stored in the browser's local storage.
Each time the user accesses the web application, a call is made to the backend with that JWT. Then,using Zustand, we can check whether the user is authorized to use our app.
useAuthStore.ts
import { create } from "zustand";
import { devtools } from "zustand/middleware";
type AuthState = {
isAuthenticated: boolean;
isAdmin: boolean;
user: AuthUser | null;
isLoading: boolean;
checkAuth: () => Promise<void>;
logout: () => void;
};
export type AuthUser = {
email: string;
name: string;
roles: string[];
};
const DEFAULT_USER_STATE = {
isAuthenticated: false,
user: null,
isAdmin: false,
isLoading: false,
};
export const useAuthStore = create<AuthState>()(
devtools((set) => ({
isAuthenticated: false,
user: null,
isLoading: true,
checkAuth: async () => {
try {
// Verify the session with the backend
const token = localStorage.getItem("myJWT");
const response = await fetch("https://api.example.com/check-auth", {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
const { email, name, roles } = data;
set({
isAuthenticated: true,
user: {
email,
name,
roles,
},
isAdmin: roles.includes("admin"),
isLoading: false,
});
} catch (error) {
console.error(error);
window.location.href = "YOUR_LOGIN_URL";
}
},
logout: () => {
set({ ...DEFAULT_USER_STATE, isLoading: true });
localStorage.removeItem("myJWT");
window.location.href = "YOUR_LOGIN_URL";
},
}))
);
PrivateRoute.tsx
import { useAuthStore } from "@/store/useAuthStore";
import { useEffect } from "react";
import { Outlet } from "react-router-dom";
const LoadingScreen = () => (
<div className="flex flex-col gap-2 items-center justify-center min-h-screen">
<span>Loading...</span>
</div>
);
export function PrivateRoute() {
const { isAuthenticated, isLoading, checkAuth } = useAuthStore();
useEffect(() => {
checkAuth();
}, [checkAuth]);
// Show loading spinner while checking authentication
if (isLoading) {
return <LoadingScreen />;
}
if (!isAuthenticated) {
window.location.href = "YOUR_LOGIN_URL";
// Show loading while redirect happens
return <LoadingScreen />;
}
return <Outlet />;
}
Happy coding!