I’m building a React + TypeScript app and using Axios for API requests. I have an AuthProvider using React Context to manage the user and token, and I’m running into a few questions and issues:
Do I need to use setToken() when the user logs in?
I’m not sure if I should store the token in state after login and how it should be used with Axios interceptors.
How to handle public routes like login/register?
My current AuthProvider runs a useEffect to fetch the current user (/auth/authorize) on app load, but this triggers errors on login/register because no token exists yet.
How can I prevent these effects from running on public pages while still fetching the user on protected pages?
How to securely store the token?
I know localStorage is an option, but I’ve heard it’s not very secure. What’s the recommended way to persist the token between page reloads in a secure manner?
import axios from "axios";
import React, {
createContext,
useContext,
useEffect,
useLayoutEffect,
useState,
} from "react";
type User = {
id: string;
username: string;
email: string;
};
type AuthContextType = {
user: User | null;
token: string | null;
setToken: (token: string | null) => void;
logout: () => void;
};
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [token, setToken] = useState<string | null>(null);
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
const fetchMe = async () => {
try {
const { data } = await axios.get(
"http://localhost:5000/auth/authorize",
{
withCredentials: true,
},
);
setUser(data.user);
} catch (err) {
setUser(null);
}
};
fetchMe();
}, []);
useLayoutEffect(() => {
const requestInterceptor = axios.interceptors.request.use(
(config) => {
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error),
);
return () => {
axios.interceptors.request.eject(requestInterceptor);
};
}, [token]);
useLayoutEffect(() => {
const responseInterceptor = axios.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
const refreshRes = await axios.post(
"/auth/refreshMyToken",
{},
{ withCredentials: true },
);
setToken(refreshRes.data.token);
return axios(error.config);
}
logout();
return Promise.reject(error);
},
);
return () => {
axios.interceptors.response.eject(responseInterceptor);
};
}, []);
const logout = () => {
setToken(null);
setUser(null);
};
return (
<AuthContext.Provider value={{ user, token, setToken, logout }}>
{children}
</AuthContext.Provider>
);
};
// ----- useAuth hook -----
export const useAuth = (): AuthContextType => {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
};