Skip to content

Next.js - Shallow Routing

Shallow routing is a feature specific to the Pages Router in Next.js. It allows you to change the URL path (including query parameters) without re-running data fetching methods like getServerSideProps, getStaticProps, or getInitialProps.

This is useful when you only need to update the URL for state that is already available on the client-side, such as tracking filters, tabs, or pagination state in the URL, without triggering a full server-side data refetch.

Note: The concept of shallow routing is less relevant in the App Router due to its different architecture focusing on Server Components and React Server Actions, where URL updates often naturally trigger server-side logic or specific component re-renders as needed.

To perform shallow routing in the Pages Router, you use the router.push or router.replace methods from the useRouter hook, passing the shallow: true option.

Let’s update a sample page (e.g., pages/index.js) to demonstrate shallow routing. Assume this page uses getServerSideProps to fetch some initial data.

import { useRouter } from 'next/router';
import Head from 'next/head';
import { useEffect, useState } from 'react';
function HomePage(props) {
const router = useRouter();
// Example client-side state that we want to reflect in the URL
const [counter, setCounter] = useState(0);
// Update counter state based on URL query parameter on initial load/route change
useEffect(() => {
if (router.query.counter) {
setCounter(parseInt(router.query.counter, 10) || 0);
}
}, [router.query.counter]);
const handleIncrement = () => {
const newCounter = counter + 1;
setCounter(newCounter);
// Update the URL query parameter without re-running getServerSideProps
router.push(`/?counter=${newCounter}`, undefined, { shallow: true });
};
return (
<>
<Head>
<title>Welcome to Next.js! (Shallow Routing)</title>
</Head>
<div>
<h1>Welcome to Next.js!</h1>
<p>Demonstrating Shallow Routing</p>
<p>Initial Stars Count (from getServerSideProps): {props.stars}</p>
<p>Client-side Counter: {counter}</p>
<button onClick={handleIncrement}>Increment Counter (Shallow Update URL)</button>
<br />
<p>Current URL Query: {JSON.stringify(router.query)}</p>
<p>(Check your browser console logs to see if getServerSideProps runs on button click)</p>
</div>
</>
);
}
export async function getServerSideProps(context) {
console.log('getServerSideProps executed!'); // Log to see when this runs
// Example: Fetch data that should only load on initial navigation or full refresh
const res = await fetch('https://api.github.com/repos/vercel/next.js');
const json = await res.json();
return {
props: { stars: json.stargazers_count || 'N/A' },
};
}
export default HomePage;

In this example:

  • We use getServerSideProps to fetch GitHub stars, logging to the console when it runs.
  • We maintain a client-side counter state.
  • An useEffect hook synchronizes the counter state if a ?counter= query parameter exists in the URL.
  • The handleIncrement function updates the client state and then uses router.push with { shallow: true } to append the new counter value to the URL’s query string.

Start the server if it’s not running:

npm run dev
# Example Output:
# ready - started server on 0.0.0.0:3000, url: http://localhost:3000
# event - compiled client and server successfully ...
  1. Open http://localhost:3000 in your browser. Check your server console (where you ran npm run dev). You should see the ‘getServerSideProps executed!’ log message.

  2. Click the ‘Increment Counter (Shallow Update URL)’ button multiple times.

  3. Observe the following:

    • The counter value displayed on the page increases.
    • The URL in the browser’s address bar updates (e.g., /?counter=1, /?counter=2, etc.).
    • Check your server console again. You should not see the ‘getServerSideProps executed!’ message logged after clicking the button. This confirms that the data fetching function did not re-run, thanks to shallow: true.
  4. Refresh the page (e.g., when the URL is /?counter=3). Now, you will see ‘getServerSideProps executed!’ in the server console because a full page load occurred. The useEffect hook will then read the counter=3 from router.query and set the initial client-side counter state accordingly.