L16: Add Sign-Out Button
Implement sign-out functionality
Let's complete the authentication system with sign-out functionality! 👋
Why Sign-Out Matters
Security reasons:
Shared computers: Next user shouldn't access your account
Public WiFi: Close session when leaving
Work computers: Don't leave account open
Privacy: End session when doneUser control:
Switch accounts: Sign out of one, into another
Testing: Developers need to test both states
Troubleshooting: "Have you tried signing out and back in?"
Peace of mind: User controls their sessionAdd Sign-Out Button to Navbar
Update Navbar with sign-out functionality:
import { Link, useNavigate } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';
function Navbar() {
const { user, signOut } = useAuth();
const navigate = useNavigate();
const handleSignOut = async () => {
await signOut();
navigate('/');
};
return (
<nav className="navbar">
{/* Brand */}
<Link to="/" className="nav-brand">
🏖️ Holidaze
</Link>
{/* Navigation Links */}
<div className="nav-links">
<Link to="/">Home</Link>
<Link to="/venues">Venues</Link>
{user ? (
<>
{/* Authenticated links */}
<Link to="/bookings">My Bookings</Link>
<Link to="/favorites">Favorites</Link>
<Link to="/venues/create">List Venue</Link>
{/* User dropdown */}
<div className="nav-user-menu">
<button className="nav-user-button">
<img
src={user.avatar?.url || '/default-avatar.png'}
alt={user.name}
className="nav-user-avatar"
/>
<span className="nav-user-name">{user.name}</span>
<span className="nav-user-icon">▼</span>
</button>
{/* Dropdown menu */}
<div className="nav-user-dropdown">
<Link to="/profile" className="dropdown-item">
<span className="dropdown-icon">👤</span>
Profile
</Link>
<Link to="/settings" className="dropdown-item">
<span className="dropdown-icon">⚙️</span>
Settings
</Link>
<Link to="/bookings" className="dropdown-item">
<span className="dropdown-icon">📅</span>
My Bookings
</Link>
<hr className="dropdown-divider" />
<button
onClick={handleSignOut}
className="dropdown-item dropdown-item-danger"
>
<span className="dropdown-icon">🚪</span>
Sign Out
</button>
</div>
</div>
</>
) : (
<>
{/* Guest links */}
<Link to="/sign-in" className="nav-link-secondary">
Sign In
</Link>
<Link to="/sign-up" className="nav-link-primary">
Sign Up
</Link>
</>
)}
</div>
</nav>
);
}
export default Navbar;Add Navbar Dropdown Styling
Style the user dropdown menu:
/* User Menu */
.nav-user-menu {
position: relative;
}
.nav-user-button {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: transparent;
border: 1px solid #e5e7eb;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
.nav-user-button:hover {
background: #f9fafb;
border-color: #d1d5db;
}
.nav-user-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
object-fit: cover;
}
.nav-user-name {
font-size: 0.875rem;
font-weight: 500;
color: #1f2937;
}
.nav-user-icon {
font-size: 0.75rem;
color: #6b7280;
transition: transform 0.2s;
}
.nav-user-menu:hover .nav-user-icon {
transform: rotate(180deg);
}
/* Dropdown */
.nav-user-dropdown {
position: absolute;
top: calc(100% + 0.5rem);
right: 0;
min-width: 200px;
background: white;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all 0.2s;
z-index: 1000;
}
.nav-user-menu:hover .nav-user-dropdown {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
/* Dropdown Items */
.dropdown-item {
display: flex;
align-items: center;
gap: 0.75rem;
width: 100%;
padding: 0.75rem 1rem;
font-size: 0.875rem;
color: #374151;
text-decoration: none;
background: transparent;
border: none;
cursor: pointer;
transition: background 0.2s;
text-align: left;
}
.dropdown-item:hover {
background: #f3f4f6;
}
.dropdown-icon {
font-size: 1.125rem;
}
.dropdown-divider {
margin: 0.5rem 0;
border: none;
border-top: 1px solid #e5e7eb;
}
.dropdown-item-danger {
color: #dc2626;
}
.dropdown-item-danger:hover {
background: #fef2f2;
color: #b91c1c;
}
/* Mobile responsive */
@media (max-width: 768px) {
.nav-user-name {
display: none;
}
.nav-user-button {
padding: 0.5rem;
}
}Update AuthContext signOut
Ensure sign-out clears all state:
import { createContext, useContext, useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import api from '@/lib/api';
import { tokenStore } from '@/lib/tokenStore';
// ... (previous code)
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [token, setToken] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const navigate = useNavigate();
// ... (previous useEffects and signIn)
const signOut = async () => {
try {
// Call logout endpoint (clears refresh token cookie)
await api.post('/auth/logout');
} catch (error) {
// Even if API call fails, sign out locally
console.error('Logout error:', error);
} finally {
// Clear local state
setUser(null);
setToken(null);
// Clear token store
tokenStore.clearToken();
// Redirect to home
navigate('/', { replace: true });
}
};
const value = { user, token, isLoading, signIn, signOut };
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};Understanding Sign-Out Flow
Step-by-step sign-out:
1. User clicks "Sign Out" button
handleSignOut() triggered
2. Call signOut() from AuthContext
await signOut();
3. Make API call to logout endpoint
await api.post('/auth/logout');
→ Server invalidates refresh token
→ Cookie deleted/marked invalid
4. Clear local state
setUser(null);
setToken(null);
tokenStore.clearToken();
5. Redirect to homepage
navigate('/', { replace: true });
6. Navbar re-renders
user === null
→ Shows "Sign In" and "Sign Up" links
7. Protected routes now inaccessible
User visits /favorites
→ Protected checks: user === null
→ Redirects to /sign-inTimeline:
0ms: User clicks "Sign Out"
10ms: signOut() starts
20ms: API call to /auth/logout
50ms: Server responds
60ms: Clear state (user, token)
65ms: Navigate to /
70ms: Navbar updates (no user)What needs to be cleared:
// 1. React state
setUser(null);
setToken(null);
// 2. Token store (for interceptor)
tokenStore.clearToken();
// 3. Any cached data (if applicable)
queryClient.clear(); // React Query
dispatch(clearCache()); // Redux
// 4. localStorage (if used)
localStorage.removeItem('preferences');
// 5. sessionStorage
sessionStorage.clear();Why each matters:
// user state
setUser(null);
// → Navbar shows guest links
// → Protected routes redirect
// → Conditional content hidden
// token state
setToken(null);
// → Context value updated
// → Components re-render
// tokenStore
tokenStore.clearToken();
// → Interceptor won't add Authorization header
// → Future API calls won't include token
// Redirect
navigate('/', { replace: true });
// → User sees public homepage
// → Back button doesn't return to protected pageCleanup order:
// ✅ Correct order
await api.post('/auth/logout'); // 1. Server first
setUser(null); // 2. Then local state
setToken(null);
tokenStore.clearToken();
navigate('/'); // 3. Finally redirect
// ❌ Wrong order
navigate('/'); // Redirect first
await api.post('/auth/logout'); // Server call after redirect
setUser(null); // State cleared after user left
// User might see flash of old state!Logout endpoint:
await api.post('/auth/logout');What server does:
1. Validates refresh token from cookie
2. Marks token as invalid in database
3. Deletes/expires refresh token cookie
4. Returns success responseResponse format:
// Success (200)
{
message: "Successfully logged out"
}
// Cookie header
Set-Cookie: refreshToken=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMTHandle errors:
const signOut = async () => {
try {
await api.post('/auth/logout');
} catch (error) {
// Server error, network error, etc.
console.error('Logout error:', error);
// Still sign out locally (important!)
// User shouldn't stay signed in if they tried to sign out
} finally {
// Always clear local state
setUser(null);
setToken(null);
tokenStore.clearToken();
navigate('/', { replace: true });
}
};Why finally block?
Scenario 1: API succeeds
→ Clear state
→ Redirect
Scenario 2: API fails (network error)
→ Still clear state (user wants to sign out!)
→ Redirect
finally ensures sign-out happens either wayWhere to redirect after sign-out:
// Option 1: Homepage (most common)
navigate('/', { replace: true });
// Option 2: Sign-in page
navigate('/sign-in', { replace: true });
// Option 3: Stay on same page (if public)
// Don't navigate, just clear state
// Only works if current page is public!
// Option 4: Last public page
const lastPublicPage = getLastPublicPage();
navigate(lastPublicPage, { replace: true });Choosing the right redirect:
const signOut = async () => {
const currentPath = location.pathname;
const publicPaths = ['/', '/about', '/venues', '/contact'];
const isCurrentPublic = publicPaths.includes(currentPath);
// Clear state...
if (isCurrentPublic) {
// Stay on current page
// No navigation needed
} else {
// Was on protected page, go home
navigate('/', { replace: true });
}
};Why replace: true:
Without replace:
User on /profile
Clicks "Sign Out"
History: [/profile, /]
Clicks back
→ /profile (but not signed in!)
→ Redirects to /sign-in
Confusing! ❌
With replace:
User on /profile
Clicks "Sign Out"
History: [/] (replaced /profile)
Clicks back
→ Previous page before /profile
Clean! ✅Alternative Sign-Out Patterns
Testing Sign-Out
Sign in first:
- Navigate to
/sign-in - Sign in with valid credentials
- Verify you're signed in (Navbar shows user menu)
Test sign-out button:
- Hover over user menu
- Dropdown should appear
- Click "Sign Out"
- Should redirect to
/(homepage)
✅ Sign-out button works!
Verify state cleared:
- Check Navbar: Should show "Sign In" and "Sign Up" links
- Open React DevTools
- Find AuthContext.Provider
- Check value:
user: should benulltoken: should benull
✅ State cleared!
Test protected route access:
- After signing out, visit
/favorites - Should redirect to
/sign-in - Should show message: "Please sign in to access /favorites"
✅ Protected routes blocked!
Test API calls without token:
- Open Network tab
- After sign-out, trigger API call
- Check request headers
- Should NOT have
Authorizationheader
✅ Token removed from requests!
Test cookie deletion:
- Sign in
- Open DevTools → Application → Cookies
- Find
refreshTokencookie - Sign out
- Cookie should be deleted or expired
✅ Refresh token removed!
Module 7 Complete! 🎉
🎉 Congratulations! You've completed Module 7: Forms & Authentication!
What you've learned:
- ✅ Context API for global state management
- ✅ React Hook Form for powerful form handling
- ✅ Zod validation for type-safe schemas
- ✅ JWT authentication with access + refresh tokens
- ✅ Axios interceptors for automatic token management
- ✅ Token refresh for seamless re-authentication
- ✅ Protected routes with route guards
- ✅ Smart redirects for great UX
- ✅ Sign-out functionality with complete cleanup
Your authentication system now includes:
- 🔐 Secure sign-in with validation
- 🎫 JWT token management (access + refresh)
- 🔄 Automatic token refresh
- 🛡️ Protected routes with guards
- 🎯 Smart redirects after sign-in
- 👋 Clean sign-out flow
- 🔒 HTTP-only cookies for security
- ⚡ Axios request/response interceptors
- 📱 Responsive, accessible UI
Next steps:
Continue to Module 8 or practice by:
- Adding password reset flow
- Implementing social sign-in
- Adding email verification
- Creating admin dashboards
- Building role-based permissions
Key Takeaways
- ✅ Sign-out button in user dropdown menu
- ✅ Hover dropdown for user menu
- ✅ API call invalidates refresh token on server
- ✅ Clear all state (user, token, token store)
- ✅ Redirect to homepage with replace
- ✅ Finally block ensures sign-out happens
- ✅ Protected routes immediately inaccessible
- ✅ Navbar updates to show guest links
- ✅ Back button doesn't return to protected pages
- ✅ Complete cleanup leaves no stale data