Guidens

Next.js router.replace Doesn’t Refresh Your Page: push vs replace vs refresh Explained Simply

softrankings · · 6 min read

Next.js router.replace Doesn’t Refresh Your Page: push vs replace vs refresh Explained Simply

A practical guide for full stack devs, with real scenarios, code, and the mistakes I see most often.

Let me share something that looks tiny but causes real bugs in real apps.

Someone logs in. The app sends them to the dashboard. They press the Back button. And they land on the login page again.

Nothing crashed. No error in the console. But the app feels broken.

Nine times out of ten, this is a router.push that should have been a router.replace. I’m writing this to explain the difference once, in simple words, so you never have to guess again.

First, a common myth

A lot of developers think router.replace reloads the page.

It doesn’t.

router.replace is client side navigation. The browser does not do a full page refresh. There’s no white flash, and your shared layout keeps its state. The Next.js docs say this directly.

So what does it change? Only one thing: your browser history.

The sticky note idea

Picture your browser history as a pile of sticky notes. Each note is a page you visited.

  • router.push('/dashboard') puts a new note on top of the pile. Press Back and you go to the note below it.

  • router.replace('/dashboard') crosses out the top note and writes the new URL on it. Press Back and you skip the page you replaced.

Same speed. Same smooth feel. Different pile.

Under the hood, push and replace map to the browser’s history.pushState and history.replaceState. That’s why the behavior is so predictable once you see it.

Where does router.refresh fit in?

There are three tools here, and people mix them up all the time.

ToolWhat it doesrouter.push(url)Go to a URL and add it to historyrouter.replace(url)Go to a URL and overwrite the current history entryrouter.refresh()Re-fetch server data for the current page, keep client state

Think of it like this. Push and replace answer “where should the user go, and can they come back?” Refresh answers “is the data on this page stale?”

They solve different problems. Don’t use one to do another’s job.

Scenarios where router.replace is the right call

1. After login or logout

'use client'
import { useRouter } from 'next/navigation'

export function LoginForm() {
  const router = useRouter()

  async function onSubmit(credentials: Credentials) {
    await signIn(credentials)
    router.replace('/dashboard')
  }

  // ...
}

The login page has done its job. Keeping it in history just gives users a way to land on a form they don’t need anymore.

2. Syncing filters, tabs or sort order to the URL

This is my favorite use. Say you have a directory with a stage filter.

function onFilterChange(stage: string) {
  const params = new URLSearchParams(searchParams.toString())
  params.set('stage', stage)

  router.replace(`${pathname}?${params.toString()}`, { scroll: false })
}

Why replace and not push? Imagine a user clicks through 12 filters. With push, leaving the page means pressing Back 12 times. That’s a trap.

Why scroll: false? By default Next scrolls to the top on navigation. When you’re just tweaking a filter, you don’t want the page to jump.

A bonus: the URL now holds the state, so people can share or bookmark the filtered view.

3. Multi step flows where earlier steps should be locked

Onboarding step 1 is done. Replace it with step 2. The user shouldn’t be able to go back and submit step 1 twice.

4. Cleaning up a bad URL

User arrives on /products/old-slug. You resolve it to /products/new-slug. Use replace so the bad URL doesn’t stay in their history.

5. Redirecting from a page the user shouldn’t see

A logged in user opens /login. Send them to the dashboard with replace. They should never be able to Back into a page that bounced them.

Scenarios where you should NOT use router.replace

This part matters just as much.

1. Normal browsing

User clicks a product card on a list page. Use push or a <Link>. Back should return them to the list. If it doesn’t, they’ll think your app is buggy.

2. Pagination people expect to step through

Page 1, 2, 3. If Back should go from page 3 to page 2, you want push.

3. When you need fresh data

Here’s the big one. replace changes the URL. It does not mean “reload this page’s data.”

Saved a profile and want the server component to show the new values?

await saveProfile(data)
router.refresh()

That’s the tool for it. Using replace to the same URL and hoping for new data is a guess, and guesses cause stale UI bugs.

4. Plain links

If it’s just a link, use <Link href="...">. You get prefetching and proper anchor tags, which also helps crawlers and accessibility. Reaching for router.replace in an onClick when a link would do is a common mistake.

5. External sites

Going to another domain? The Next router is for your app’s routes. Use window.location or a normal anchor.

6. Redirects you can do on the server

In a server component or route handler, use redirect(). It’s cleaner than rendering something, sending the user to the client, and then bouncing them.

Gotchas I’d flag

Debounce search inputs. If you call router.replace on every keystroke, you’ll fire a navigation for each letter. Wait 300 to 500ms after the user stops typing.

Don’t call replace inside an effect without a guard. A useEffect that replaces the URL, which changes a dependency, which triggers the effect again, is a loop. Check the condition first.

useSearchParams needs a Suspense boundary in many setups. If you read search params in a client component for these filter patterns, wrap it in <Suspense> so you don’t hit build or rendering warnings.

Wrap in a transition if you want a pending state. useTransition lets you show a small loading indicator while the navigation resolves:

const [isPending, startTransition] = useTransition()

startTransition(() => {
  router.replace(`${pathname}?${params.toString()}`, { scroll: false })
})

The one question rule

Before any navigation in your code, ask this:

“Should the Back button return to this page?”

  • Yes, it should: use push

  • No, it shouldn’t: use replace

  • I just need updated data: use refresh

That covers most of what you’ll hit in production.

Why this matters more than it looks

Users treat the Back button like an undo. When your app hijacks it, they won’t file a bug report. They’ll just feel something is off, and trust drops a little.

Small details like this separate an app that works from an app that feels right. And as a full stack dev, this is exactly the kind of detail that sits between your frontend and your product thinking.

So go check your codebase today. Search for router.push after login, after form submit, and on filter changes. I’d bet you find at least one that should be replace.

I’m sharing this because I wish someone had explained it to me this simply earlier. If it helped, save it for later and share it with a dev friend who still thinks replace reloads the page.

What’s the weirdest back button bug you’ve seen?

Comments

?

No comments yet

Be the first to share what you think.