L15: Redirect When Signed In
Implement smart redirects for better UX
Let's implement intelligent redirect logic for a seamless user experience! 🎯
The Redirect Problem
Current behavior (not ideal):
User visits /favorites (not signed in)
↓
Redirects to /sign-in
↓
User signs in
↓
Redirects to / (homepage)
↓
User has to navigate to /favorites again 😞Desired behavior:
User visits /favorites (not signed in)
↓
Redirects to /sign-in (saves intended destination)
↓
User signs in
↓
Redirects to /favorites (where they wanted to go!)
↓
User is exactly where they wanted to be 😊Update SignInPage with Redirect
Implement smart redirect after sign-in:
import { useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';
import SignInForm from '@/components/SignInForm';
function SignInPage() {
const { user } = useAuth();
const location = useLocation();
const navigate = useNavigate();
// Get the page they were trying to access
const from = location.state?.from?.pathname || '/';
// If already signed in, redirect immediately
useEffect(() => {
if (user) {
navigate(from, { replace: true });
}
}, [user, from, navigate]);
// Don't show sign-in form if already signed in
if (user) {
return null; // Or a brief "Redirecting..." message
}
return (
<div className="sign-in-page">
<div className="sign-in-container">
<div className="sign-in-header">
<h1 className="brand-logo">🏖️ Holidaze</h1>
<p className="brand-tagline">Your next adventure awaits</p>
</div>
<div className="sign-in-form-container">
<h2 className="sign-in-title">Sign in to your account</h2>
{/* Show message if redirected from protected page */}
{location.state?.from && (
<p className="redirect-message">
Please sign in to access <strong>{from}</strong>
</p>
)}
<p className="sign-in-subtitle">
Welcome back! Please enter your details.
</p>
<SignInForm />
</div>
<div className="sign-in-footer">
<p className="footer-text">
Don't have an account?{' '}
<a href="/sign-up" className="footer-link">
Sign up
</a>
</p>
</div>
</div>
</div>
);
}
export default SignInPage;Update SignInForm to Handle Redirect
Modify the form to redirect after successful sign-in:
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useLocation, useNavigate } from 'react-router-dom';
import { signInSchema } from '@/schemas/signInSchema';
import { useAuth } from '@/contexts/AuthContext';
function SignInForm() {
const { signIn } = useAuth();
const [apiError, setApiError] = useState('');
const location = useLocation();
const navigate = useNavigate();
// Get intended destination
const from = location.state?.from?.pathname || '/';
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm({
resolver: zodResolver(signInSchema),
});
const onSubmit = async (data) => {
setApiError('');
const result = await signIn(data.email, data.password);
if (result.success) {
// Success! Redirect to intended page
navigate(from, { replace: true });
} else {
// Show error
setApiError(result.error);
}
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="sign-in-form">
{apiError && (
<div className="api-error" role="alert">
<p>{apiError}</p>
</div>
)}
<div className="form-field">
<label htmlFor="email" className="form-label">
Email
</label>
<input
id="email"
type="email"
{...register('email')}
className={`form-input ${errors.email ? 'form-input-error' : ''}`}
placeholder="you@example.com"
/>
{errors.email && (
<p className="form-error">{errors.email.message}</p>
)}
</div>
<div className="form-field">
<label htmlFor="password" className="form-label">
Password
</label>
<input
id="password"
type="password"
{...register('password')}
className={`form-input ${errors.password ? 'form-input-error' : ''}`}
placeholder="••••••••"
/>
{errors.password && (
<p className="form-error">{errors.password.message}</p>
)}
</div>
<button
type="submit"
disabled={isSubmitting}
className="submit-button"
>
{isSubmitting ? 'Signing in...' : 'Sign In'}
</button>
</form>
);
}
export default SignInForm;Update AuthContext (Remove Auto-Redirect)
Remove automatic redirect from signIn since we handle it in the form:
export const AuthProvider = ({ children }) => {
// ... existing state and effects
const signIn = async (email, password) => {
try {
const response = await api.post('/auth/login', {
email,
password,
});
const { accessToken, data } = response.data;
setToken(accessToken);
setUser(data);
// ❌ Remove this:
// navigate('/');
// ✅ Return success, let caller handle navigation
return { success: true };
} catch (error) {
const message = error.response?.data?.errors?.[0]?.message
|| 'Invalid email or password';
return { success: false, error: message };
}
};
// ... rest of provider
};Add Redirect Message Styling
Style the redirect message:
/* Redirect Message */
.redirect-message {
padding: 0.875rem 1rem;
background: #eff6ff;
border: 1px solid #bfdbfe;
border-radius: 8px;
margin-bottom: 1rem;
color: #1e40af;
font-size: 0.875rem;
text-align: center;
}
.redirect-message strong {
font-weight: 600;
color: #1e3a8a;
}Understanding the Flow
Complete redirect flow:
1. User visits /favorites (not signed in)
location = { pathname: '/favorites', state: null }
2. Protected component checks auth
user === null → Not authenticated
3. Redirect with state
<Navigate
to="/sign-in"
state={{ from: location }}
replace
/>
4. Now at /sign-in
location = {
pathname: '/sign-in',
state: { from: { pathname: '/favorites' } }
}
5. SignInPage reads state
const from = location.state?.from?.pathname || '/';
// from = '/favorites'
6. User signs in successfully
navigate(from, { replace: true });
// Navigates to '/favorites'
7. User is on /favorites (authenticated)
Protected component checks auth
user !== null → Authenticated
Renders FavoritesPage ✅State preservation:
// In Protected component
<Navigate
to="/sign-in"
state={{ from: location }} // Pass entire location
replace
/>
// In SignInPage
const from = location.state?.from?.pathname;
// Extracts pathname from state
// Supports complex locations
location = {
pathname: '/search',
search: '?q=beach&guests=4',
hash: '#results'
};
// After sign-in, restore full location
navigate({
pathname: from.pathname,
search: from.search,
hash: from.hash
}, { replace: true });Scenario 1: Direct access to protected route
User types URL: /favorites
↓
Not signed in
↓
Redirect to /sign-in (save /favorites)
↓
Sign in
↓
Redirect to /favorites ✅Scenario 2: Click link to protected route
User on homepage
↓
Clicks "Favorites" link
↓
Not signed in
↓
Redirect to /sign-in (save /favorites)
↓
Sign in
↓
Redirect to /favorites ✅Scenario 3: Direct access to sign-in
User types URL: /sign-in
↓
No "from" in state
↓
Sign in
↓
Redirect to / (homepage) ✅Scenario 4: Already signed in
User signed in → Visits /sign-in
↓
useEffect detects user
↓
Redirect to / immediately ✅Scenario 5: Multiple redirects
User visits /bookings (not signed in)
↓
Redirect to /sign-in (save /bookings)
↓
User navigates away to /about
↓
User goes back to /sign-in
↓
"from" still saved in state
↓
Sign in
↓
Redirect to /bookings ✅Edge case 1: Sign-in page as redirect target
// Problem: Redirect to /sign-in from /sign-in causes loop
const from = location.state?.from?.pathname;
// Solution: Don't redirect to /sign-in
const from = location.state?.from?.pathname === '/sign-in'
? '/'
: location.state?.from?.pathname || '/';Edge case 2: Invalid redirect URL
// Problem: from = '/admin' but user doesn't have access
const from = location.state?.from?.pathname || '/';
// Solution: Validate redirect
const isValidRedirect = (path) => {
const publicPaths = ['/', '/about', '/venues'];
const protectedPaths = ['/favorites', '/bookings', '/profile'];
return [...publicPaths, ...protectedPaths].includes(path);
};
const from = isValidRedirect(location.state?.from?.pathname)
? location.state?.from?.pathname
: '/';Edge case 3: Query parameters in redirect
// User was at /search?q=beach&guests=4
// Preserve full URL
const from = location.state?.from;
// Restore complete location
if (from) {
navigate({
pathname: from.pathname,
search: from.search,
hash: from.hash
}, { replace: true });
} else {
navigate('/', { replace: true });
}Edge case 4: Nested protected routes
// User visits /dashboard/analytics (protected)
// Redirects to /sign-in with state
// After sign-in, should go to /dashboard/analytics
// Works automatically with current implementation!
const from = location.state?.from?.pathname;
// from = '/dashboard/analytics'
navigate(from, { replace: true });
// Navigates to '/dashboard/analytics' ✅Understanding replace:
// Without replace (push)
navigate('/sign-in');
History stack:
/favorites ← Was here
/sign-in ← Now here
User clicks back:
/favorites ← Goes here (not signed in, redirects again!)
/sign-in ← Redirect loop!
// With replace
navigate('/sign-in', { replace: true });
History stack:
/sign-in ← Now here (replaced /favorites)
User clicks back:
/ (or previous page before /favorites) ← Goes here ✅When to use replace:
// ✅ Redirects (avoid back-button loops)
navigate('/sign-in', { replace: true });
navigate('/', { replace: true });
// ✅ After form submission
handleSubmit = async () => {
await createVenue(data);
navigate('/venues/mine', { replace: true });
};
// ✅ After sign-in/sign-out
signIn = async () => {
// ...
navigate(from, { replace: true });
};
// ❌ Regular navigation (keep in history)
<Link to="/about">About</Link>
// User should be able to go backReplace in Protected component:
<Navigate
to="/sign-in"
state={{ from: location }}
replace // ← Important!
/>Why replace here?
Without replace:
User visits /favorites
History: [/, /favorites]
Redirects to /sign-in
History: [/, /favorites, /sign-in]
Signs in, goes to /favorites
History: [/, /favorites, /sign-in, /favorites]
Clicks back
→ /sign-in (already signed in, redirects to /)
→ /favorites
Confusing navigation! ❌
With replace:
User visits /favorites
History: [/, /favorites]
Redirects to /sign-in (replaces /favorites)
History: [/, /sign-in]
Signs in, goes to /favorites (replaces /sign-in)
History: [/, /favorites]
Clicks back
→ / (clean!) ✅Testing Redirect Flow
Test redirect from protected route:
- Sign out
- Visit
/favoritesdirectly - Should redirect to
/sign-in - Check URL:
/sign-in - Page should show: "Please sign in to access /favorites"
✅ Redirect message shown!
Test sign-in and redirect back:
- Fill sign-in form
- Submit
- Should redirect to
/favorites - Page should show FavoritesPage
✅ Redirected to intended page!
Test direct sign-in (no redirect):
- Sign out
- Visit
/sign-indirectly - Sign in
- Should redirect to
/(homepage)
✅ Default redirect to homepage!
Test already signed in:
- Ensure you're signed in
- Visit
/sign-inin new tab - Should immediately redirect to
/ - Sign-in form should not appear
✅ Already signed-in redirect working!
Test history navigation:
- Sign out
- Visit
/about - Visit
/favorites(redirects to/sign-in) - Sign in (redirects to
/favorites) - Click browser back button
- Should go to
/about(not/sign-in)
✅ History navigation clean!
Advanced Redirect Patterns
What's Next?
In Lesson 16, we'll:
- Add sign-out functionality
- Create sign-out button in Navbar
- Handle sign-out redirect
- Clean up state on sign-out
✅ Lesson Complete! Smart redirects implemented for seamless UX!
Key Takeaways
- ✅ Location state preserves intended destination
- ✅ Redirect after sign-in to where user wanted to go
- ✅ Default to homepage if no intended destination
- ✅ Show message indicating why sign-in required
- ✅ Redirect if already signed in to avoid confusion
- ✅ Use
replace: trueto prevent back-button issues - ✅ Preserve query parameters for complete URL restoration
- ✅ Handle edge cases (invalid redirects, loops)
- ✅ Conditional redirects based on user role
- ✅ Clean history navigation with proper replace usage