If you ever used Pages Router from Next.js you might be familiar with an ability to do shallow navigation, in App Router this option is gone from router hooks. Currently when you change anything in URL it goes to the Next.js server everytime, it might be unnecessary in cases for example when you have some client only components that does not render on server, but you still want to persist the state in URL (and you probably should do pretty much always, it enhances UX), to prevent that you have to find a magic trick...
And the trick is... just use plain old browser native History API, easier said than done but let's see...
// root page.tsx
import { Suspense } from 'react';
import { ShallowParams } from './shallow-params';
// simple counter so you will clearly see that another
// roundtrip to server occured
let serverRenderCount = 0;
export default async function Home() {
console.log('Server run', serverRenderCount++);
return (
<div className="flex flex-col">
{/* Suspense is needed because we are using useSearchParams hook which would fallback Next.js to dynamic rendering always */}
<Suspense>
<ShallowParams />
</Suspense>
</div>
);
}
// shallow-params.tsx
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
export function ShallowParams() {
// useSearchParams updates state even if we use History API
const params = useSearchParams();
const router = useRouter();
const count = +(params.get('count') || 0);
return (
<div className="flex flex-col gap-2">
<p>{params.get('count') || 0}</p>
<button
className="btn"
onClick={() => {
// shallow increase count params via the push. Check the browser back and forward buttons.
window.history.pushState({}, '', `/?count=${count + 1}`);
}}
>
Add to count +1 (push)
</button>
<button
className="btn"
onClick={() => {
// shallow increase count params via the replace. Check the browser back and forward buttons, the history was overridden
window.history.replaceState({}, '', `/?count=${count + 5}`);
}}
>
Add to count +5 (replace)
</button>
<button
className="btn"
onClick={() => {
// and this is a normal router action, in the server console you will see the serverRenderCount increase and log, with above methods it won't log anything.
router.push(`/?count=${count + 10}`);
}}
>
Add to count +10 (Next.js router push)
</button>
</div>
);
}
And that's pretty much it. Here is a link to StackBlitz sandbox you can play with the example from above
You can build abstractions around it and do whatever you want with shallow routing. For simple things it would be okay to use History API directly, but as always web development have many rough edge cases that would pop out at the least expected moment, that said, there is an excellent library out there called nuqs
it makes search params first class citizen in Next.js (and other frameworks also!) and makes sure you won't encounter those rough edge cases.
That's it for today. Take a look at my other articles where I share some nice tricks!
Top comments (0)