Nextjs Link onClick: Run Code, Redirect, and Handle Forms

Authors
  • avatar
    Name
    Hamza Rahman
Published on
-
7 mins read
-

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.

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.

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 ClickExample

TypeScript 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 ClickExample

For the Pages Router, change the import to import { useRouter } from 'next/router' and drop the 'use client' line.

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

  1. Use router.push() for normal navigation so the back button works as expected.
  2. Use router.replace() for login and logout flows, so users cannot navigate back into a stale authenticated or signed-out state.
  3. Only call preventDefault() when you intend to handle navigation yourself. If you just want a side effect on click, let Link navigate normally.
  4. Handle errors around async work before you redirect, so a failed request does not leave the user on a dead end.
  5. Let Link prefetch for you. It prefetches links in the viewport automatically, so reach for router.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.