A deep dive into debugging React + Next.js App Router re-render issues
January 4, 2026 | notion-clone project
Capture render logs and network traces to see when components remount.
Race conditions, pathname sync, Zustand isolation, basePath — ruled out systematically.
Replace router.replace() with history.replaceState() to keep SPA state intact.
When user clicks a task, both setActiveTaskId and router.replace run. The URL sync effect then re-sets activeTaskId, causing a double update.
Add an intentRef to track user-initiated actions and skip URL sync during active transitions.
Close worked. Open still flickered. RunningTasksBar still flickered on both. This indicated the race condition was a symptom, not the root cause.
usePathname() triggers re-renders when URL changes. 7+ components use it via useTaskHref:
Create TaskUrlSync component outside Board tree to handle URL sync, move activeTaskId to Zustand.
Isolation didn't help because something else was causing Board to completely remount.
Maybe Zustand state updates were still cascading re-renders through the component tree.
Move activeTaskId entirely to Zustand, Board reads only from store, no usePathname anywhere.
Still flickered! Time to add debug logging to trace actual render sequence.
Child components use useTaskHref which internally uses usePathname. Store basePath in Zustand once.
STILL FLICKERED! We were treating symptoms, not the disease. Debug logs revealed the truth...
Adding console.log to track render counts revealed the smoking gun:
The render counts reset to #1! Components were UNMOUNTED and REMOUNTED.
router.replace('/task/:id') causes Next.js to attempt navigation to a route that doesn't exist.
Use window.history.replaceState() instead of router.replace()
| Aspect | router.replace() | window.history.replaceState() |
|---|---|---|
| Triggers Next.js navigation | ✗ Yes | ✓ No |
| Server fetch | ✗ Yes (fails) | ✓ None |
| Component remount | ✗ Yes | ✓ None |
| Updates browser URL | ✓ Yes | ✓ Yes |
| Shareable/bookmarkable | ✓ Yes | ✓ Yes |
| SPA experience | ✗ Broken | ✓ Perfect |
True SPA behavior achieved.
| # | Hypothesis | Solution Tried | Result |
|---|---|---|---|
| 1 | Race condition (state & URL) | Guard effect with intentRef | ❌ Partial |
| 2 | usePathname causes re-renders | Isolate URL sync | ❌ Failed |
| 3 | Zustand updates cascade | Move activeTaskId to Zustand | ❌ Failed |
| 4 | useTaskHref uses usePathname | Move basePath to Zustand | ❌ Failed |
| 🏆 | router.replace triggers remount | Use window.history.replaceState | ✓ Success! |
Lesson learned: When debugging re-render issues in Next.js App Router, check if your navigation method is causing server component re-execution. Adding render count logging early would have revealed the remount pattern sooner.