The Next.js Link component handles client-side navigation, but you often need to run some code at the moment a user clicks: log an event, validate a form, update state, or decide where to send them. This guide shows how to use the onClick prop on Link, how preventDefault interacts with navigation, and how to redirect programmatically. Examples cover both the App Router and the Pages Router, in JavaScript and TypeScript.
Does next/link support an onClick prop?
Yes. Link accepts an onClick handler and passes it to the underlying anchor element. Your handler runs first, and then Next.js performs the client-side navigation. If you call e.preventDefault() inside the handler, Next.js skips its own navigation, which lets you take full control and navigate yourself.
import Link from 'next/link'
export default function Nav() { return ( <Link href="/about" onClick={() => { console.log('About link clicked') }} > About </Link> )}In modern Next.js (13 and newer) you put onClick directly on Link. You do not need a nested <a> tag anymore. If you are on Next.js 12 or older, see the legacy note at the end.
Run code on click, then let Link navigate
The simplest case: do something on click and let Link handle the navigation as usual. Do not call preventDefault here, because you want the normal navigation to happen.
import Link from 'next/link'
export default function ProductLink({ id, name }) { const trackClick = () => { // analytics, state update, anything synchronous window.gtag?.('event', 'select_item', { item_id: id }) }
return ( <Link href={`/product/${id}`} onClick={trackClick}> {name} </Link> )}Nextjs onClick redirect with preventDefault
When you need to run logic and then decide where to go, cancel the built-in navigation with e.preventDefault() and push the route yourself with useRouter.
In the App Router, useRouter comes from next/navigation, and the component must be a Client Component ('use client').
'use client'import Link from 'next/link'import { useRouter } from 'next/navigation'
export default function ClickExample({ link }) { const router = useRouter()
const handleClick = (event) => { event.preventDefault() // run any logic first router.push(link) }
return ( <Link href={link} onClick={handleClick}> Handle click </Link> )}router.push(link) adds the new route to the browser history, so the back button returns to the current page. If you do not want that entry in history, use router.replace(link) instead.
Pages Router version
If your app uses the Pages Router, the only difference is the import path. useRouter comes from next/router, and you do not need the 'use client' directive.
import Link from 'next/link'import { useRouter } from 'next/router'
function ClickExample({ link }) { const router = useRouter()
const handleClick = (event) => { event.preventDefault() router.push(link) }
return ( <Link href={link} onClick={handleClick}> Handle click </Link> )}
export default ClickExampleTypeScript version
The handler receives a standard React mouse event. Type it as React.MouseEvent<HTMLAnchorElement>, since Link renders an anchor.
'use client'import Link from 'next/link'import { useRouter } from 'next/navigation'
interface ClickExampleProps { link: string}
const ClickExample = ({ link }: ClickExampleProps) => { const router = useRouter()
const handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => { event.preventDefault() router.push(link) }
return ( <Link href={link} onClick={handleClick}> Handle click </Link> )}
export default ClickExampleFor the Pages Router, change the import to import { useRouter } from 'next/router' and drop the 'use client' line.
Use a Link as a button
A common question is how to make a Next.js button that navigates on click. If the end result is going to another page, keep using Link and style it like a button. That preserves the things a real link gives you for free: middle-click and "open in new tab", and keyboard and screen-reader support that a plain <button> with a redirect loses.
import Link from 'next/link'
export default function CheckoutButton() { return ( <Link href="/checkout" onClick={() => window.gtag?.('event', 'begin_checkout')} className="inline-block rounded bg-blue-600 px-4 py-2 text-white" > Checkout </Link> )}If the click does not navigate at all, like submitting a form or opening a modal, use a real <button> instead. Reach for a button-styled Link only when the click takes the user to another route.
Redirect on click in plain React (without Next.js)
Some readers land here wanting an onClick redirect in a plain React app that does not use Next.js. The idea is the same, only the navigation call changes. With React Router, use the useNavigate hook:
import { useNavigate } from 'react-router-dom'
function CheckoutButton() { const navigate = useNavigate()
const handleClick = () => { // run any logic first navigate('/checkout') }
return <button onClick={handleClick}>Checkout</button>}Without a router you can set window.location.href = '/checkout' for a full page load, but you lose client-side navigation. Prefer your router's navigate function whenever you have one.
Login form: redirect after submit and prefetch
A common pattern is redirecting after a successful API call. Here Link is not the trigger, the form is, but the routing approach is the same. The example also prefetches the destination so the transition feels instant.
import { useEffect, useState } from 'react'import { useRouter } from 'next/router'import axios from 'axios'
export default function Login() { const router = useRouter() const [username, setUsername] = useState('') const [password, setPassword] = useState('')
const handleSubmit = async (e) => { e.preventDefault()
if (username && password) { try { const response = await axios.post( '/api/login', { username, password }, { headers: { 'Content-Type': 'application/json' } } ) if (response.status === 200) { router.push('/home') } } catch (error) { console.error('Login failed:', error) } } }
useEffect(() => { // Prefetch the home page for a faster transition router.prefetch('/home') }, [router])
return ( <form onSubmit={handleSubmit}> <input type="text" name="username" value={username} onChange={(e) => setUsername(e.target.value)} /> <input type="password" name="password" value={password} onChange={(e) => setPassword(e.target.value)} /> <button type="submit">Login</button> </form> )}On success, router.push('/home') sends the user to the homepage. On failure you could push them to an error page instead. router.prefetch('/home') loads the target route in the background so navigation is near instant. Remember that useRouter is a hook, so it only works inside a function component.
In the App Router, swap the import to next/navigation and call router.prefetch('/home') the same way. Prefetching also happens automatically for any Link in the viewport, so you often do not need to call it by hand.
Best practices
- Use
router.push()for normal navigation so the back button works as expected. - Use
router.replace()for login and logout flows, so users cannot navigate back into a stale authenticated or signed-out state. - Only call
preventDefault()when you intend to handle navigation yourself. If you just want a side effect on click, letLinknavigate normally. - Handle errors around async work before you redirect, so a failed request does not leave the user on a dead end.
- Let
Linkprefetch for you. It prefetches links in the viewport automatically, so reach forrouter.prefetch()only for routes that are likely but not currently visible.
Legacy note for Next.js 12 and older
Before Next.js 13, Link did not render its own anchor. You had to nest an <a> tag and put onClick on it:
// Next.js 12 and older<Link href="/about"> <a onClick={handleClick}>About</a></Link>If you upgrade to Next.js 13 or newer, remove the nested <a> and move onClick, className, and similar props straight onto Link. The older form still works if you set legacyBehavior, but the direct form is the current default.
Related articles
Deploy Next.js SSR to Cloudflare Workers with GitHub Actions
I tried to deploy my Next.js SSR app and hit the usual snags (Pages 404s, works-on-my-machine). Here’s the simple CI/CD flow I shipped with Cloudflare Workers + GitHub Actions.
Cheerio vs node-html-parser: which HTML parser to use
Cheerio vs node-html-parser for parsing HTML in Node.js: node-html-parser is lighter and faster, Cheerio has a fuller jQuery-style API. How to choose between them.
Cheerio vs Puppeteer: which to use for scraping
Cheerio vs Puppeteer for web scraping in Node.js: Cheerio parses static HTML and is fast, Puppeteer drives a real browser and handles JavaScript-rendered pages. When to use each, and how to combine them.

