Logout is not just clearing localStorage — a complete logout revokes the refresh token on the server (preventing it from being used to get new access tokens), clears all local state, invalidates the RTK Query cache (so no stale user-specific data remains), and navigates to the home page. The “logout from all devices” variant invalidates every refresh token for the user, forcing re-authentication on all browser tabs, mobile apps, and other sessions.
Complete Logout Implementation
// src/stores/authStore.js — logout action (complete implementation)
logout: async () => {
const refreshToken = localStorage.getItem("refresh_token");
// 1. Revoke the refresh token on the server
// Best effort — proceed even if the API call fails
// (the token will expire naturally if we can't revoke it)
if (refreshToken) {
try {
await authApi.logout(refreshToken);
} catch {
// Silent fail — server may be unreachable
// The token's TTL will handle expiry anyway
}
}
// 2. Clear tokens from storage
localStorage.removeItem("refresh_token");
// 3. Clear Zustand state (persist middleware also clears accessToken from localStorage)
set({ user: null, accessToken: null, isLoading: false });
// Note: RTK Query cache clearing is done in the LogoutButton component
// (it needs access to dispatch, which is not available in the Zustand store)
},
Note: The RTK Query cache cannot be cleared from inside the Zustand store because clearing the cache requires
dispatch(blogApi.util.resetApiState()) which needs the Redux dispatch function. The Zustand store does not have access to Redux. The solution is to call the RTK cache clear from the React component or hook that handles logout — it has access to both useDispatch and the Zustand logout action. This is an intentional architectural separation: Zustand handles auth state, Redux handles API cache state.Tip: Add a “Logout from all devices” button to the Profile page that calls
POST /api/auth/logout-all. On the FastAPI side, this deletes all refresh tokens for the user from the database, invalidating every active session. The current session also becomes invalid, so the user is redirected to login. This is particularly useful when a user suspects their account has been accessed from an unknown device.Warning: Clearing the RTK Query cache with
resetApiState() removes all cached data for all endpoints — not just user-specific data. Public cached data (posts list, tags) is also cleared. On the next page load, all data will be refetched. This is acceptable for logout (the user is going to the home page anyway), but if you want surgical cache invalidation (only clear user-specific data), invalidate specific tags: dispatch(blogApi.util.invalidateTags([{ type: "User", id: "ME" }])) instead of resetting everything.LogoutButton Component
// src/components/auth/LogoutButton.jsx
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
import { useAuthStore } from "@/stores/authStore";
import { blogApi } from "@/store/apiSlice";
import { useToast } from "@/context/ToastContext";
export function LogoutButton({ className = "" }) {
const dispatch = useDispatch();
const navigate = useNavigate();
const logout = useAuthStore((s) => s.logout);
const toast = useToast();
async function handleLogout() {
try {
// 1. Revoke token + clear Zustand state
await logout();
// 2. Clear RTK Query cache (needs Redux dispatch)
dispatch(blogApi.util.resetApiState());
// 3. Navigate to home
navigate("/", { replace: true });
toast.info("You have been signed out");
} catch {
// Even if server call fails, proceed with local logout
navigate("/", { replace: true });
}
}
return (
<button
onClick={handleLogout}
className={`text-gray-600 hover:text-gray-900 ${className}`}
>
Sign Out
</button>
);
}
Logout from All Devices
// src/pages/ProfilePage.jsx (partial)
function LogoutAllDevices() {
const dispatch = useDispatch();
const navigate = useNavigate();
const logout = useAuthStore((s) => s.logout);
const toast = useToast();
const [isLoading, setLoading] = useState(false);
async function handleLogoutAll() {
if (!window.confirm("Sign out of all devices? You will need to log in again.")) return;
setLoading(true);
try {
await authApi.logoutAll(); // POST /api/auth/logout-all
dispatch(blogApi.util.resetApiState());
logout(); // clear local tokens too
navigate("/login", { replace: true });
toast.success("Signed out from all devices");
} catch {
toast.error("Failed to sign out from all devices");
} finally {
setLoading(false);
}
}
return (
<div className="border border-red-100 rounded-lg p-4">
<h3 className="font-medium text-red-700 mb-1">Security</h3>
<p className="text-sm text-gray-500 mb-3">
Sign out of all browser tabs, mobile apps, and other devices.
</p>
<button onClick={handleLogoutAll} disabled={isLoading}
className="px-4 py-2 bg-red-600 text-white text-sm
rounded hover:bg-red-700 disabled:opacity-50">
{isLoading ? "Signing out…" : "Sign Out from All Devices"}
</button>
</div>
);
}
Common Mistakes
Mistake 1 — Not clearing RTK Query cache on logout
❌ Wrong — user A logs out, user B logs in and sees user A’s cached data:
await logout();
navigate("/"); // RTK Query still has user A's dashboard data!
✅ Correct — clear the cache before navigating:
await logout();
dispatch(blogApi.util.resetApiState()); // ✓ clear all cached data
navigate("/", { replace: true });
Mistake 2 — Not revoking refresh token on server
❌ Wrong — attacker with stolen refresh token can still get new access tokens:
// Client-side only:
localStorage.removeItem("access_token"); // token is revoked locally...
localStorage.removeItem("refresh_token"); // ...but still valid on server!
✅ Correct — always call the server logout endpoint before clearing local storage.
Quick Reference — Logout Checklist
| Step | Code | Why |
|---|---|---|
| Revoke server token | POST /api/auth/logout |
Prevents stolen token reuse |
| Clear localStorage | localStorage.removeItem("refresh_token") |
Remove local credentials |
| Clear Zustand | set({ user: null, accessToken: null }) |
Reset auth state |
| Clear RTK cache | dispatch(blogApi.util.resetApiState()) |
No stale user data for next user |
| Navigate | navigate("/", { replace: true }) |
Return to public page |