I store the Sanctum token in localStorage. That survives a page reload. React state does not. So on every reload my app knows it might have a session, but it knows nothing about the user — no name for the sidebar, no role for the route guards, nothing.

My fix is boring on purpose: one endpoint, GET /api/auth/me, and one useEffect that runs before anything else.

useEffect(() => { const stored = localStorage.getItem('saas_token'); if (!stored) { setLoading(false); return; } client .get('/auth/me') .then((res) => { setUser(res.data.user); setToken(stored); }) .catch(() => { // Token is dead (revoked or forged) — throw it away. localStorage.removeItem('saas_token'); setUser(null); setToken(null); }) .finally(() => setLoading(false)); }, []); 

Three decisions hiding in there:

1. No token, no request. If localStorage is empty we skip the network call entirely and drop straight out of the loading state. A logged-out visitor never pays for a request that can only fail.

2. The endpoint returns the same UserResource as login. me() responds with ['user' => new UserResource($request->user())] — the exact same user shape the frontend got at login. There is no second code path for "logged in via reload".

3. A failed restore cleans house. A 401 on /me means the token is revoked or forged. We delete it from localStorage instead of keeping a zombie token that would fail every subsequent request and confuse the 401 interceptor. The user lands on the login page with a clean slate, not a broken session.

The loading flag matters more than it looks. My protected route waits on it — a spinner gate I wrote about earlier — so a refresh never flashes the login page for a user who is about to be restored. Restore first, route second.

Small, unglamorous, and every SPA with token auth needs it. This pattern lives in the Laravel + React SaaS Starter Kit I maintain, where the auth context, the /me endpoint and the route guards ship wired together: https://kamranofficial.gumroad.com/l/mhekoig