While Link components handle declarative navigation (clicking links), sometimes you need to navigate programmatically in response to events—form submissions, button clicks, API responses, or timers. The useRouter hook gives you full control over navigation in Client Components. You can push new pages, replace history entries, go back/forward, refresh data, and prefetch routes programmatically. Let's master programmatic navigation!
Basic useRouter Usage
Import useRouter from next/navigation and use it in Client Components:
'use client';
import { useRouter } from 'next/navigation';
export function NavigationButton() {
const router = useRouter();
const handleClick = () => {
// Navigate to /about
router.push('/about');
};
return (
<button
onClick={handleClick}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Go to About
</button>
);
}
// ✅ 'use client' directive required
// ✅ Import from 'next/navigation' (not 'next/router')
// ✅ Call router.push() to navigate
// ✅ Works in response to any event⚠️ Client Components Only
useRouter only works in Client Components. For Server Components, use the redirect() function instead.
// Server Component
import { redirect } from 'next/navigation';
async function ServerPage() {
const user = await getUser();
if (!user) {
redirect('/login'); // Use redirect() in Server Components
}
return <div>Welcome!</div>;
}Router Methods
1. router.push() - Navigate with History
Add a new entry to the browser history:
'use client';
import { useRouter } from 'next/navigation';
export function NavigationExample() {
const router = useRouter();
return (
<div className="space-y-4">
{/* Navigate to different pages */}
<button onClick={() => router.push('/blog')}>
Go to Blog
</button>
<button onClick={() => router.push('/about')}>
Go to About
</button>
{/* Navigate with query parameters */}
<button onClick={() => router.push('/search?q=nextjs')}>
Search for Next.js
</button>
{/* Navigate to dynamic route */}
<button onClick={() => router.push(`/blog/${postId}`)}>
View Post
</button>
</div>
);
}
// ✅ Adds to browser history
// ✅ User can use back button
// ✅ Supports query parameters
// ✅ Works with dynamic routes2. router.replace() - Replace History Entry
Replace the current history entry (user can't go back):
'use client';
import { useRouter } from 'next/navigation';
export function LoginForm() {
const router = useRouter();
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
// Login logic
const success = await login(email, password);
if (success) {
// Replace login page with dashboard
// User can't go back to login page
router.replace('/dashboard');
}
};
return (
<form onSubmit={handleLogin}>
{/* Form fields */}
<button type="submit">Login</button>
</form>
);
}
// Use router.replace() for:
// ✅ Post-login redirects
// ✅ Form submission redirects
// ✅ Intermediate/temporary pages
// ✅ When back button shouldn't work
// ❌ Don't use for normal navigation
// User expects back button to work3. router.back() - Go Back
'use client';
import { useRouter } from 'next/navigation';
export function BackButton() {
const router = useRouter();
return (
<button
onClick={() => router.back()}
className="flex items-center gap-2 text-gray-600 hover:text-gray-900"
>
<span>←</span>
Back
</button>
);
}
// ✅ Navigates to previous page in history
// ✅ Same as browser back button
// ✅ Useful for modal close, cancel actions4. router.forward() - Go Forward
'use client';
import { useRouter } from 'next/navigation';
export function HistoryButtons() {
const router = useRouter();
return (
<div className="flex gap-2">
<button onClick={() => router.back()}>
← Back
</button>
<button onClick={() => router.forward()}>
Forward →
</button>
</div>
);
}
// ✅ Navigates to next page in history
// ✅ Same as browser forward button
// ✅ Only works if user went back first5. router.refresh() - Refresh Data
Re-fetch Server Component data without losing client state:
'use client';
import { useRouter } from 'next/navigation';
export function RefreshButton() {
const router = useRouter();
const handleRefresh = () => {
// Re-fetch server data for current route
// Preserves client state (form inputs, scroll position)
router.refresh();
};
return (
<button
onClick={handleRefresh}
className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
>
🔄 Refresh
</button>
);
}
// When to use router.refresh():
// ✅ After creating/updating/deleting data
// ✅ After form submission
// ✅ Manual refresh button
// ✅ Polling for updates
// Example: After creating a post
const handleSubmit = async () => {
await createPost(data);
router.refresh(); // Show new post in list
};
// ✅ No full page reload
// ✅ Preserves client state
// ✅ Re-fetches Server Component data6. router.prefetch() - Prefetch Route
'use client';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
export function PrefetchExample() {
const router = useRouter();
useEffect(() => {
// Prefetch dashboard when component mounts
router.prefetch('/dashboard');
}, [router]);
return (
<button onClick={() => router.push('/dashboard')}>
Go to Dashboard (Prefetched)
</button>
);
}
// ✅ Manually prefetch routes
// ✅ Useful for conditional prefetching
// ✅ Route loads instantly when clicked
// Example: Prefetch on hover
const handleMouseEnter = () => {
router.prefetch('/dashboard');
};
<button
onMouseEnter={handleMouseEnter}
onClick={() => router.push('/dashboard')}
>
Dashboard
</button>Practical Examples
Example 1: Login Form with Redirect
'use client';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
export function LoginForm() {
const router = useRouter();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const data = await response.json();
if (response.ok) {
// Login successful - redirect to dashboard
// Use replace so user can't go back to login page
router.replace('/dashboard');
} else {
setError(data.message || 'Login failed');
}
} catch (err) {
setError('An error occurred. Please try again.');
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit} className="max-w-md mx-auto space-y-4">
<h2 className="text-2xl font-bold mb-6">Login</h2>
{error && (
<div className="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded">
{error}
</div>
)}
<div>
<label className="block text-sm font-medium mb-2">
Email
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">
Password
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400"
>
{loading ? 'Logging in...' : 'Login'}
</button>
</form>
);
}
// ✅ Form submission triggers navigation
// ✅ Uses router.replace() to prevent back to login
// ✅ Handles loading and error states
// ✅ Professional user experienceExample 2: Search Bar with Navigation
'use client';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
export function SearchBar() {
const router = useRouter();
const [query, setQuery] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (query.trim()) {
// Navigate to search results page with query
router.push(`/search?q=${encodeURIComponent(query)}`);
}
};
const handleClear = () => {
setQuery('');
// Go back to current page without query
router.push(window.location.pathname);
};
return (
<form onSubmit={handleSubmit} className="relative max-w-lg">
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
className="w-full px-4 py-2 pr-24 border rounded-lg focus:ring-2 focus:ring-blue-500"
/>
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex gap-2">
{query && (
<button
type="button"
onClick={handleClear}
className="text-gray-500 hover:text-gray-700"
>
✕
</button>
)}
<button
type="submit"
className="px-4 py-1 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Search
</button>
</div>
</form>
);
}
// ✅ Form submission navigates to search results
// ✅ Query parameter in URL
// ✅ Clear button removes query
// ✅ URL encoding for special charactersExample 3: Multi-Step Form with Navigation
'use client';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
type Step = 'shipping' | 'payment' | 'confirmation';
export default function CheckoutPage() {
const router = useRouter();
const [step, setStep] = useState<Step>('shipping');
const [formData, setFormData] = useState({
name: '',
address: '',
cardNumber: '',
});
const handleNext = () => {
if (step === 'shipping') {
setStep('payment');
} else if (step === 'payment') {
setStep('confirmation');
}
};
const handleBack = () => {
if (step === 'payment') {
setStep('shipping');
} else if (step === 'confirmation') {
setStep('payment');
}
};
const handleComplete = async () => {
// Process order
await submitOrder(formData);
// Replace checkout with success page
// User can't go back to checkout
router.replace('/order/success');
};
return (
<div className="container mx-auto px-4 py-8 max-w-2xl">
<h1 className="text-3xl font-bold mb-8">Checkout</h1>
{/* Progress Indicator */}
<div className="flex justify-between mb-8">
<div className={`flex-1 text-center ${step === 'shipping' ? 'text-blue-600 font-bold' : 'text-gray-400'}`}>
1. Shipping
</div>
<div className={`flex-1 text-center ${step === 'payment' ? 'text-blue-600 font-bold' : 'text-gray-400'}`}>
2. Payment
</div>
<div className={`flex-1 text-center ${step === 'confirmation' ? 'text-blue-600 font-bold' : 'text-gray-400'}`}>
3. Confirm
</div>
</div>
{/* Step Content */}
{step === 'shipping' && (
<div className="space-y-4">
<h2 className="text-2xl font-bold mb-4">Shipping Information</h2>
<input
type="text"
placeholder="Full Name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="w-full px-4 py-2 border rounded-lg"
/>
<input
type="text"
placeholder="Address"
value={formData.address}
onChange={(e) => setFormData({ ...formData, address: e.target.value })}
className="w-full px-4 py-2 border rounded-lg"
/>
</div>
)}
{step === 'payment' && (
<div className="space-y-4">
<h2 className="text-2xl font-bold mb-4">Payment Information</h2>
<input
type="text"
placeholder="Card Number"
value={formData.cardNumber}
onChange={(e) => setFormData({ ...formData, cardNumber: e.target.value })}
className="w-full px-4 py-2 border rounded-lg"
/>
</div>
)}
{step === 'confirmation' && (
<div className="space-y-4">
<h2 className="text-2xl font-bold mb-4">Confirm Order</h2>
<div className="bg-gray-50 p-4 rounded-lg space-y-2">
<p><strong>Name:</strong> {formData.name}</p>
<p><strong>Address:</strong> {formData.address}</p>
<p><strong>Card:</strong> **** **** **** {formData.cardNumber.slice(-4)}</p>
</div>
</div>
)}
{/* Navigation Buttons */}
<div className="flex justify-between mt-8">
<button
onClick={step === 'shipping' ? () => router.back() : handleBack}
className="px-6 py-3 border rounded-lg hover:bg-gray-50"
>
{step === 'shipping' ? 'Cancel' : 'Back'}
</button>
{step === 'confirmation' ? (
<button
onClick={handleComplete}
className="px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700"
>
Complete Order
</button>
) : (
<button
onClick={handleNext}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Next
</button>
)}
</div>
</div>
);
}
// ✅ Multi-step form with state
// ✅ Back/Next navigation
// ✅ router.replace() on completion
// ✅ Can't go back after order completeExample 4: Conditional Navigation After API Call
'use client';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
export function CreatePostButton({ userId }: { userId: string }) {
const router = useRouter();
const [loading, setLoading] = useState(false);
const handleCreatePost = async () => {
setLoading(true);
try {
const response = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId,
title: 'New Post',
content: 'Draft content',
}),
});
const data = await response.json();
if (response.ok) {
// Navigate to the newly created post for editing
router.push(`/blog/edit/${data.slug}`);
// Refresh the blog list in background
router.refresh();
} else {
alert('Failed to create post');
}
} catch (error) {
alert('An error occurred');
} finally {
setLoading(false);
}
};
return (
<button
onClick={handleCreatePost}
disabled={loading}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400"
>
{loading ? 'Creating...' : 'Create New Post'}
</button>
);
}
// ✅ API call then navigation
// ✅ Navigate to new resource
// ✅ Refresh to show changes
// ✅ Loading state during API calluseRouter Examples Structure
Project structure with useRouter usage patterns
Select a file or folder to see details
Common Navigation Patterns
Pattern 1: Navigate After Successful Action
const handleSubmit = async () => {
const success = await saveData(data);
if (success) {
router.push('/success');
} else {
// Show error, stay on page
}
};Pattern 2: Navigate with Confirmation
const handleDelete = async () => {
if (confirm('Are you sure you want to delete this?')) {
await deleteItem(id);
router.push('/items'); // Go back to list
router.refresh(); // Refresh the list
}
};Pattern 3: Timed Redirect
useEffect(() => {
// Redirect after 3 seconds
const timer = setTimeout(() => {
router.push('/home');
}, 3000);
return () => clearTimeout(timer);
}, [router]);Pattern 4: Conditional Redirect on Mount
useEffect(() => {
// Check authentication
const checkAuth = async () => {
const user = await getUser();
if (!user) {
router.replace('/login');
}
};
checkAuth();
}, [router]);useRouter Best Practices
1. Use router.replace() for One-Way Flows
// ✅ GOOD: Replace for login redirects
const handleLogin = async () => {
await login();
router.replace('/dashboard'); // Can't go back to login
};
// ❌ BAD: Push for login (user can go back)
const handleLogin = async () => {
await login();
router.push('/dashboard'); // User can press back to login
};2. Handle Loading States
// ✅ GOOD: Show loading during navigation
const [loading, setLoading] = useState(false);
const handleClick = async () => {
setLoading(true);
await saveData();
router.push('/success');
// Note: Component might unmount, so no setLoading(false)
};
return (
<button disabled={loading}>
{loading ? 'Saving...' : 'Save'}
</button>
);3. Use router.refresh() After Mutations
// ✅ GOOD: Refresh after creating/updating/deleting
const handleCreate = async () => {
await createPost(data);
router.refresh(); // Show new post in list
};
const handleUpdate = async () => {
await updatePost(id, data);
router.refresh(); // Show updated data
};
const handleDelete = async () => {
await deletePost(id);
router.push('/blog'); // Go to list
router.refresh(); // Update the list
};4. Don't Navigate in Render
// ❌ BAD: Navigating during render
function MyComponent() {
const router = useRouter();
if (condition) {
router.push('/other'); // Don't do this!
}
return <div>Content</div>;
}
// ✅ GOOD: Navigate in effect or event handler
function MyComponent() {
const router = useRouter();
useEffect(() => {
if (condition) {
router.push('/other');
}
}, [condition, router]);
return <div>Content</div>;
}5. Clean Up Timers
// ✅ GOOD: Clean up timers on unmount
useEffect(() => {
const timer = setTimeout(() => {
router.push('/home');
}, 3000);
return () => clearTimeout(timer); // Clean up
}, [router]);Key Takeaways
- Client Components only - needs 'use client' directive
- Import from 'next/navigation' - not 'next/router'
- router.push() - navigate with history (can go back)
- router.replace() - replace history (can't go back)
- router.back()/forward() - browser history navigation
- router.refresh() - re-fetch data without reload
- Use for event handlers - form submits, button clicks
- Handle loading states - show feedback during navigation
What's Next?
You've mastered programmatic navigation with useRouter! Next, we'll explore usePathname and useSearchParams Hooks—how to access the current pathname and search parameters in Client Components. You'll learn to read the current URL, work with query parameters, and build navigation UI that responds to the current route.
These hooks are essential for active link highlighting, reading filters from the URL, conditional rendering based on the current route, and more.
🎯 When to Use Each Method
Use router.push() for most navigation. Use router.replace() only when you specifically don't want users to go back. Think of replace as "this page shouldn't exist in history."