Logout and Session Management — Clean Token Revocation

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

🧠 Test Yourself

User A logs out without clearing the RTK Query cache. User B logs in on the same browser. What is the risk when User B visits their dashboard?