useRouter & Programmatic Navigation
Sometimes navigation needs to happen as a result of code running, not as a direct response to a user clicking a link — after a form successfully submits, after an authentication check completes, or after a timer expires. That's what the useRouter hook is for.
Getting Access to the Router
useRouter is a Client Component hook — it relies on browser APIs and React state under the hood, so the component calling it must include the 'use client' directive.
'use client'
import { useRouter } from 'next/navigation'
export function BackButton() {
const router = useRouter()
return <button onClick={() => router.back()}>Go back</button>
}The Router Methods
Method | What It Does |
|---|---|
router.push(href) | Navigates to a new route, adding an entry to the browser history. |
router.replace(href) | Navigates to a new route without adding a history entry — the back button skips it. |
router.back() | Navigates to the previous entry in the history stack, like the browser back button. |
router.forward() | Navigates forward in the history stack. |
router.refresh() | Re-fetches the current route from the server, refreshing Server Component data without losing client state. |
Worked Example: Redirect After Submit
A very common pattern is sending a user to a confirmation or detail page right after a form submission succeeds.
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
export function CreatePostForm() {
const router = useRouter()
const [title, setTitle] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
async function handleSubmit(event: React.FormEvent) {
event.preventDefault()
setIsSubmitting(true)
const response = await fetch('/api/posts', {
method: 'POST',
body: JSON.stringify({ title }),
})
const { id } = await response.json()
setIsSubmitting(false)
// Navigate to the new post's page once creation succeeds.
router.push(`/blog/${id}`)
}
return (
<form onSubmit={handleSubmit}>
<input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Post title" />
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Saving...' : 'Create Post'}
</button>
</form>
)
}push vs replace
Use replace instead of push for flows where you don't want the user to be able to navigate back to the previous screen — a classic example is redirecting away from a login page after a successful sign-in, so the back button doesn't return the user to the login form.
'use client'
import { useRouter } from 'next/navigation'
export function LoginForm() {
const router = useRouter()
async function handleLogin() {
// ...authenticate...
router.replace('/dashboard')
}
return <button onClick={handleLogin}>Log in</button>
}When to Reach for router.refresh()
refresh() is handy when a client-side action changes server data (say, through an API route) and you want the currently rendered Server Components to reflect that change, without a full navigation and without losing any client-side state like scroll position or open dropdowns.
push and replace both accept the same href formats as Link.
useRouter only works inside Client Components — it cannot be called in a Server Component.
For links a user clicks directly, prefer the Link component over router.push in an onClick.
Server Actions often make router.push unnecessary — the redirect() function (covered later) can handle navigation directly from server code.
useRouter is the escape hatch for navigation that needs to happen inside your own logic rather than declaratively in JSX. Combined with the Link component, it covers essentially every navigation scenario you'll run into in an App Router application.