Next.js Navigation: Link Component vs router.push() vs HTML a Tag – Key Differences Explained for Newbies
Navigation is the backbone of any web application. It’s how users move between pages, interact with content, and experience your app. For newcomers to Next.js, the framework’s navigation options can feel overwhelming. Should you use the standard HTML <a> tag? The Next.js-specific Link component? Or the programmatic router.push() method?
In this blog, we’ll break down these three navigation approaches, explain their inner workings, highlight key differences, and guide you on when to use each. By the end, you’ll confidently choose the right tool for any navigation scenario in your Next.js app.
Table of Contents#
- What is Next.js Navigation?
- The HTML
<a>Tag: The "Old Reliable" - Next.js
LinkComponent: Optimized for Performance - The
router.push()Method: Programmatic Control - Key Differences: A Side-by-Side Comparison
- When to Use Which?
- Common Pitfalls & Best Practices
- Conclusion
- References
What is Next.js Navigation?#
Before diving into the tools, let’s clarify how navigation works in Next.js. Traditional websites use server-side navigation: clicking a link sends a request to the server, which returns a new HTML page—causing a full page reload. This is slow and disrupts the user experience (e.g., losing scroll position or app state).
Next.js prioritizes client-side navigation, where only the necessary page content is fetched and updated in the browser, mimicking a single-page app (SPA) feel. This reduces load times and preserves state. The Link component and router.push() are Next.js’s tools to enable this—while the HTML <a> tag sticks to server-side navigation.
The HTML <a> Tag: The "Old Reliable"#
The <a> tag is the standard HTML element for navigation. You’ve likely used it:
<a href="https://example.com">Go to Example</a>How It Works in Next.js#
In Next.js, using <a> behaves the same way as in vanilla HTML: clicking it triggers a full page reload. The browser sends a request to the server, downloads the entire new page, and resets the app state (e.g., React component state, context, or browser scroll position).
When to Use It#
- External Links: Always use
<a>for links to external websites (e.g.,href="https://google.com"). Next.js’sLinkcomponent is designed for internal navigation and may misbehave with external URLs. - Server-Side Navigation Intent: Rarely, you might intentionally want a full page reload (e.g., to reset all client-side state).
Limitations for Internal Navigation#
- Performance Hit: Full page reloads increase load time and harm user experience.
- State Loss: Client-side state (e.g., form inputs, scroll position) is reset.
- No Prefetching: Next.js can’t optimize loading for
<a>tags as it does for its native tools.
Next.js Link Component: Optimized for Internal Navigation#
The Link component is Next.js’s primary tool for internal navigation. It’s imported from next/link and is designed to enable client-side navigation with minimal overhead.
How It Works#
Link intercepts clicks on its child element (traditionally an <a> tag) and uses client-side routing to load the new page. This avoids full page reloads, keeping the app fast and stateful.
Key Features:#
- Client-Side Navigation: Updates the URL and renders the new page without reloading the browser.
- Automatic Prefetching: In production,
Linkprefetches the linked page’s assets (HTML, JS, CSS) when the link is visible in the viewport. This makes navigation feel instant. - SEO-Friendly: Still renders a standard
<a>tag in the DOM, so search engines can crawl links normally.
Usage Examples#
1. Pages Router (Older Next.js Versions)#
In the Pages Router (Next.js <13, or projects using the pages/ directory), Link requires an <a> tag as its direct child:
import Link from 'next/link';
function Navbar() {
return (
<nav>
{/* Internal link using Link component */}
<Link href="/about">
<a>About Us</a> {/* <a> tag is required here */}
</Link>
</nav>
);
}2. App Router (Next.js 13+)#
In the App Router (using the app/ directory), Link is more flexible. It no longer requires an <a> tag—you can wrap text, buttons, or other elements directly:
import Link from 'next/link';
function Navbar() {
return (
<nav>
{/* Simplified: No <a> tag needed in App Router */}
<Link href="/about">About Us</Link>
{/* Still works with <a> if needed */}
<Link href="/contact">
<a className="contact-link">Contact</a>
</Link>
</nav>
);
}Key Props#
href: The path to navigate to (e.g.,/about,/blog/[slug]for dynamic routes).prefetch: Optional boolean (true/false). Controls whether Next.js prefetches the page (default:truein production for visible links).replace: Iftrue, replaces the current history entry instead of adding a new one (similar torouter.push(..., { replace: true })).
Best Practices#
- Use Relative Paths: Prefer relative
hrefvalues (e.g.,/about) over absolute URLs (e.g.,http://yourapp.com/about). - Avoid External URLs: Stick to
<a>tags for external links to prevent unexpected behavior.
router.push(): Programmatic Navigation#
While Link is great for static navigation (e.g., menu links), router.push() is used for programmatic navigation—triggering navigation based on user actions or logic (e.g., after form submission, button clicks, or API responses).
How It Works#
router.push() is a method provided by Next.js’s router object. It programmatically navigates to a new URL, using client-side navigation by default.
Usage Examples#
1. Pages Router#
In the Pages Router, import useRouter from next/router:
import { useRouter } from 'next/router';
function LoginForm() {
const router = useRouter();
const handleSubmit = async (e) => {
e.preventDefault();
const isAuthenticated = await loginUser(); // Your auth logic
if (isAuthenticated) {
// Navigate to dashboard after successful login
router.push('/dashboard');
// Optional: Replace current history entry (avoids "back" to login)
// router.push('/dashboard', undefined, { replace: true });
}
};
return <form onSubmit={handleSubmit}>...</form>;
}2. App Router#
In the App Router, import useRouter from next/navigation (note the different import path!):
import { useRouter } from 'next/navigation';
function AddToCartButton() {
const router = useRouter();
const handleAddToCart = async () => {
await addItemToCart(); // Your cart logic
// Navigate to cart page after adding item
router.push('/cart');
};
return <button onClick={handleAddToCart}>Add to Cart</button>;
}Key Features#
- Dynamic Control: Trigger navigation based on conditions (e.g., form validation, API success).
- History Management: Use the
replaceoption to replace the current history entry (e.g.,router.push('/dashboard', { replace: true })), preventing users from navigating back to the previous page. - Shallow Routing (Pages Router Only): Update the URL without re-running
getStaticPropsorgetServerSideProps(use cautiously—complex to manage).
Limitations#
- No Prefetching: Unlike
Link,router.push()does not prefetch pages, so navigation may feel slower for large pages. - Requires Router Instance: You need to call
useRouter()in a component (can’t use it outside React components without workarounds).
Key Differences: A Side-by-Side Comparison#
To clarify when to use each tool, here’s a breakdown of their core differences:
| Feature | HTML <a> Tag | Next.js Link Component | router.push() |
|---|---|---|---|
| Navigation Type | Server-side (full page request) | Client-side | Client-side |
| Page Reload | Yes (full reload) | No (partial DOM update) | No (partial DOM update) |
| Prefetching | No | Yes (production, visible links) | No |
| Primary Use Case | External links | Static internal navigation | Programmatic/dynamic navigation |
| Control | Declarative (HTML) | Declarative (React component) | Imperative (JavaScript method) |
| State Preservation | No (resets client state) | Yes (preserves state) | Yes (preserves state) |
| External Links | Ideal (use for external URLs) | Not recommended (may break) | Not recommended (may break) |
When to Use Which?#
-
Use
<a>tag if:- You’re linking to an external website (e.g.,
href="https://twitter.com"). - You intentionally want a full page reload.
- You’re linking to an external website (e.g.,
-
Use
Linkcomponent if:- You’re navigating between internal pages (e.g., menu links, footer links).
- You want automatic performance optimizations like prefetching.
- You need declarative navigation (i.e., navigation triggered by a user clicking a link).
-
Use
router.push()if:- Navigation depends on logic (e.g., after form submission, API success, or button clicks).
- You need dynamic control (e.g., conditionally navigating to different pages).
Common Pitfalls & Best Practices#
Even experienced developers trip up with Next.js navigation. Here are key pitfalls to avoid:
1. Misusing Link with <a> Tags#
- Pages Router: Always wrap an
<a>tag insideLink(e.g.,<Link href="/about"><a>About</a></Link>). Forgetting the<a>tag will break navigation. - App Router:
Linkcan wrap text/elements directly (no<a>needed), but you can still use<a>for styling or accessibility.
2. Using External URLs with Link#
Never use Link for external URLs (e.g., <Link href="https://google.com">). Use <a> instead—Link may add unnecessary client-side routing logic and cause errors.
3. Overusing router.push() for Static Navigation#
If navigation is triggered by a simple link click (not logic), prefer Link over router.push(). Link is more declarative, readable, and optimized with prefetching.
4. Ignoring replace in router.push()#
For actions like login/signup, use router.push('/dashboard', { replace: true }) to replace the login page in history. This prevents users from clicking "back" and returning to the login screen after authentication.
5. Shallow Routing Confusion (Pages Router)#
Shallow routing (router.push('/page?query=1', undefined, { shallow: true })) updates the URL without re-running data-fetching methods like getStaticProps. However, it only works for the same page. Misusing it for cross-page navigation will cause bugs.
Conclusion#
Next.js offers three primary navigation tools, each with distinct use cases:
<a>tag: For external links or intentional full page reloads.Linkcomponent: For static internal navigation with automatic performance boosts.router.push(): For dynamic, programmatic navigation triggered by logic.
By choosing the right tool for the job, you’ll build fast, user-friendly apps that feel seamless. As a newbie, start with Link for most internal links, <a> for external, and router.push() when you need to navigate based on user actions or conditions.