[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"quiz-navigation-api-intercept-navigation":44,"search-suggestions":60,"quiz-article-navigation-api-intercept-navigation":107},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"id":45,"slug":46,"kind":47,"title":48,"description":49,"config":50,"verticalId":5,"vertical":55,"course":52,"_count":56,"access":57,"attempts":59,"questionCount":51},"01a04446-501a-7095-8534-775f8a6d3c76","navigation-api-intercept-navigation","PRACTICE_QUIZ","Navigation API: intercept before it commits","Check what actually changes when you replace popstate listening with the Navigation API's navigate event.",{"questionCount":51,"timeLimitSec":52,"shuffleQuestions":18,"shuffleOptions":17,"negativeMarking":19,"passScorePct":53,"maxAttempts":52,"revealAnswers":54,"allowFlagging":18,"allowBacktracking":17},8,null,70,"AFTER_SUBMIT",{"slug":6,"name":7},{"questions":51},{"allowed":17,"reason":58},"FREE",[],[61,65,69,73,77,81,85,89,92,96,100,104],{"slug":62,"name":63,"articles":64},"webdev","Webdev",77,{"slug":66,"name":67,"articles":68},"frontend","Frontend",65,{"slug":70,"name":71,"articles":72},"javascript","Javascript",64,{"slug":74,"name":75,"articles":76},"css","Css",29,{"slug":78,"name":79,"articles":80},"tutorial","Tutorial",16,{"slug":82,"name":83,"articles":84},"typescript","Typescript",11,{"slug":86,"name":87,"articles":88},"performance","Performance",10,{"slug":90,"name":91,"articles":51},"react","React",{"slug":93,"name":94,"articles":95},"grammar","Grammar",6,{"slug":97,"name":98,"articles":99},"node","Node",5,{"slug":101,"name":102,"articles":103},"programming","Programming",4,{"slug":105,"name":106,"articles":103},"html","Html",{"id":108,"slug":46,"title":109,"subtitle":52,"excerpt":110,"coverUrl":111,"locale":13,"readingMinutes":99,"publishedAt":112,"viewCount":113,"likeCount":19,"commentCount":19,"author":114,"vertical":119,"topic":120,"tags":122,"_count":129,"playground":131,"body":133,"bodyMd":262,"seo":263,"translationGroupId":266,"series":52,"podcastUrl":52,"verticalId":5,"thread":267,"assessments":269,"translations":272,"quiz":274},"01a04446-4fc4-73e1-b763-eda8c9cf631f","Your Router Doesn't Intercept Navigation. It Reacts To It.","Double-click a link during a slow route change and most SPA routers render two pages and settle on the wrong one — because they're listening for navigation after it already happened. The Navigation API lets you stop it before it starts.","\u002Fmedia\u002Fcovers\u002Fnavigation-api-intercept-navigation.png","2026-09-01T11:23:43.875Z",32,{"id":115,"name":116,"username":117,"avatarUrl":52,"headline":118},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":70,"name":121},"JavaScript",[123,124,125,128],{"slug":70,"name":71,"color":52},{"slug":62,"name":63,"color":52},{"slug":126,"name":127,"color":52},"browser","Browser",{"slug":66,"name":67,"color":52},{"assessments":130},1,{"slug":46,"title":132},"Navigation race — interactive playground",{"blocks":134,"version":130},[135,139,142,145,150,153,159,162,165,168,171,175,178,182,185,189,192,199,202,205,208,211,214,217,220,223,226,229,232,235,238,241,244,247,250,253,256],{"id":136,"html":137,"type":138},"b1","\u003Cp>Click a link. Before the new page finishes loading, click a different one. You&#39;d expect the second click to win — that&#39;s what happens with a plain \u003Ccode>&lt;a href&gt;\u003C\u002Fcode> and a full page load. In a lot of single-page apps, it doesn&#39;t.\u003C\u002Fp>","paragraph",{"id":140,"html":141,"type":138},"b2","\u003Cp>What actually happens: the first navigation starts fetching data. The second navigation starts too. Whichever fetch resolves last wins, regardless of which link you clicked last. For a second, the URL bar says one page and the rendered content says another. On a slow connection, &quot;for a second&quot; can be long enough for someone to screenshot it and file a bug titled &quot;app shows wrong page.&quot;\u003C\u002Fp>",{"id":143,"html":144,"type":138},"b3","\u003Cp>Here&#39;s the part that surprises people: this isn&#39;t a bug in your router. It&#39;s the shape of the tool.\u003C\u002Fp>",{"id":146,"html":147,"text":148,"type":149,"level":31},"b4","How \u003Ccode>pushState\u003C\u002Fcode> routing actually works","How pushState routing actually works","heading",{"id":151,"html":152,"type":138},"b5","\u003Cp>Every client-side router — the ones you&#39;ve built by hand and the ones bundled into frameworks — is doing roughly this:\u003C\u002Fp>",{"id":154,"code":155,"type":156,"language":157,"highlight":158},"b6","document.addEventListener(\"click\", (e) => {\n  const link = e.target.closest(\"a\");\n  if (!link || !isInternal(link)) return;\n\n  e.preventDefault();\n  history.pushState({}, \"\", link.href);\n  renderRoute(link.href); \u002F\u002F fetch data, swap the view\n});\n\nwindow.addEventListener(\"popstate\", () => {\n  renderRoute(location.href); \u002F\u002F back\u002Fforward button\n});","code","js",[],{"id":160,"html":161,"type":138},"b7","\u003Cp>Read that again and notice what&#39;s missing: nothing here knows about the \u003Cem>previous\u003C\u002Fem> \u003Ccode>renderRoute\u003C\u002Fcode> call while a new one starts. \u003Ccode>pushState\u003C\u002Fcode> already changed the URL by the time \u003Ccode>renderRoute\u003C\u002Fcode> begins — the navigation, as far as the browser&#39;s history is concerned, already happened. Your router isn&#39;t intercepting it. It&#39;s reacting to something already committed, then racing its own async work against whatever the last reaction was doing.\u003C\u002Fp>",{"id":163,"html":164,"type":138},"b8","\u003Cp>That&#39;s the double-click bug in one sentence: two \u003Ccode>renderRoute\u003C\u002Fcode> calls, no relationship between them, last one to finish wins. Add a canceled \u003Ccode>fetch\u003C\u002Fcode> and an \u003Ccode>AbortController\u003C\u002Fcode> to the mix and you can make it \u003Cem>mostly\u003C\u002Fem> go away — mature router libraries do exactly that. But you&#39;re patching a race condition from outside the event that caused it, because the click handler and the URL change were never one atomic thing to begin with.\u003C\u002Fp>",{"id":166,"html":167,"text":167,"type":149,"level":31},"b9","The API built to fire before the URL changes",{"id":169,"html":170,"type":138},"b10","\u003Cp>The \u003Ca href=\"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FNavigation_API\">Navigation API\u003C\u002Fa> exists specifically to close that gap. Instead of listening for \u003Ccode>popstate\u003C\u002Fcode> after a change, you listen for \u003Ccode>navigate\u003C\u002Fcode> on \u003Ccode>window.navigation\u003C\u002Fcode> — and that event fires \u003Cem>before\u003C\u002Fem> the browser commits to the new URL, for every kind of navigation: link clicks, back\u002Fforward, \u003Ccode>history.pushState\u003C\u002Fcode>, even form submissions.\u003C\u002Fp>",{"id":172,"code":173,"type":156,"language":157,"highlight":174},"b11","navigation.addEventListener(\"navigate\", (event) => {\n  if (!event.canIntercept) return; \u002F\u002F e.g. cross-origin navigations can't be\n\n  const url = new URL(event.destination.url);\n\n  event.intercept({\n    async handler() {\n      await renderRoute(url); \u002F\u002F the browser waits on this\n    },\n  });\n});",[],{"id":176,"html":177,"type":138},"b12","\u003Cp>\u003Ccode>intercept()\u003C\u002Fcode> is the piece \u003Ccode>popstate\u003C\u002Fcode> never had: it hands the browser a promise and the browser \u003Cem>waits on it\u003C\u002Fem> before treating the navigation as finished — no spinner state you have to fake, no separate &quot;is this route still current&quot; flag to check by hand. And because every \u003Ccode>navigate\u003C\u002Fcode> event carries its own \u003Ccode>event.destination\u003C\u002Fcode>, a second click firing a second \u003Ccode>navigate\u003C\u002Fcode> event doesn&#39;t need to guess whether an earlier one is still in flight — you can call \u003Ccode>event.signal\u003C\u002Fcode> (an \u003Ccode>AbortSignal\u003C\u002Fcode> the browser aborts automatically when a newer navigation supersedes this one) inside your handler and pass it straight to \u003Ccode>fetch\u003C\u002Fcode>:\u003C\u002Fp>",{"id":179,"code":180,"type":156,"language":157,"highlight":181},"b13","event.intercept({\n  async handler() {\n    const data = await fetch(url, { signal: event.signal }).then(r => r.json());\n    render(data);\n  },\n});",[],{"id":183,"html":184,"type":138},"b14","\u003Cp>Click a second link before the first \u003Ccode>handler\u003C\u002Fcode> resolves, and the browser aborts the first navigation&#39;s signal for you. The stale fetch throws, you skip rendering it, and there&#39;s no manual bookkeeping to get wrong — the race condition isn&#39;t patched, it&#39;s structurally not possible to hit.\u003C\u002Fp>",{"id":186,"html":187,"text":188,"type":149,"level":31},"b15","What you get that \u003Ccode>pushState\u003C\u002Fcode> never gave you","What you get that pushState never gave you",{"id":190,"html":191,"type":138},"b16","\u003Cp>A few things fall out of treating navigation as one interceptable event instead of a click handler plus a \u003Ccode>popstate\u003C\u002Fcode> listener bolted on the side:\u003C\u002Fp>",{"id":193,"type":194,"items":195,"ordered":18},"b17","list",[196,197,198],"\u003Cstrong>One event for everything.\u003C\u002Fstrong> Link clicks, back\u002Fforward, and programmatic \u003Ccode>navigation.navigate()\u003C\u002Fcode> calls all go through the same \u003Ccode>navigate\u003C\u002Fcode> event — you&#39;re not maintaining a click handler and a separate \u003Ccode>popstate\u003C\u002Fcode> handler that have to agree with each other.","\u003Cstrong>Cancellation that&#39;s actually cancellation.\u003C\u002Fstrong> \u003Ccode>event.preventDefault()\u003C\u002Fcode> on \u003Ccode>navigate\u003C\u002Fcode> stops the navigation before the URL changes, not after — useful for &quot;you have unsaved changes&quot; guards that used to require faking the URL back with another \u003Ccode>pushState\u003C\u002Fcode> call.","\u003Cstrong>Real navigation state.\u003C\u002Fstrong> \u003Ccode>navigation.currentEntry\u003C\u002Fcode>, \u003Ccode>navigation.entries()\u003C\u002Fcode>, and events like \u003Ccode>navigatesuccess\u003C\u002Fcode> \u002F \u003Ccode>navigateerror\u003C\u002Fcode> give you a queryable list of history entries with keys and state, instead of \u003Ccode>history.length\u003C\u002Fcode> (a number that tells you nothing about \u003Cem>what&#39;s\u003C\u002Fem> in the stack).",{"id":200,"html":201,"type":138},"b18","\u003Cp>None of this replaces routing logic — you still decide what URL maps to what view. It replaces the part where the browser&#39;s actual navigation and your app&#39;s idea of navigation are two loosely-synced systems held together by \u003Ccode>preventDefault\u003C\u002Fcode> and hope.\u003C\u002Fp>",{"id":203,"html":204,"text":204,"type":149,"level":31},"b19","Where it stands today",{"id":206,"html":207,"type":138},"b20","\u003Cp>The Navigation API has shipped in Chrome and Edge since 2022. Safari and Firefox don&#39;t have it yet as of this writing, which is the honest caveat: this isn&#39;t a drop-in replacement for your router today, and any production use needs a fallback path (or a router library that already detects and uses it under the hood, falling back to \u003Ccode>pushState\u003C\u002Fcode> where it&#39;s missing). Check current support before reaching for it directly in anything beyond a Chromium-only tool.\u003C\u002Fp>",{"id":209,"html":210,"type":138},"b21","\u003C!-- playground:start -->",{"id":212,"html":213,"text":213,"type":149,"level":31},"b22","🎮 Try it yourself",{"id":215,"html":216,"type":138},"b23","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnavigation-api-intercept-navigation\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":218,"html":219,"type":138},"b24","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":221,"html":222,"type":138},"b25","\u003C!-- playground:end -->",{"id":224,"html":225,"text":225,"type":149,"level":31},"b26","The one thing worth remembering",{"id":227,"html":228,"type":138},"b27","\u003Cp>\u003Ccode>popstate\u003C\u002Fcode> tells you a navigation happened. \u003Ccode>navigate\u003C\u002Fcode> tells you one is \u003Cem>about\u003C\u002Fem> to, and lets you decide what &quot;happened&quot; even means. That difference is the whole reason the double-click race exists in the first place — and the whole reason it stops existing once the browser is holding the promise instead of you holding a flag.\u003C\u002Fp>",{"id":230,"html":231,"type":138},"b28","\u003Cp>Next time your router&#39;s navigation logic starts growing \u003Ccode>isNavigating\u003C\u002Fcode> booleans and manual \u003Ccode>AbortController\u003C\u002Fcode> bookkeeping, that&#39;s usually this gap showing through. Have you hit the double-navigation race in production, or did your router already paper over it? I&#39;d like to know which libraries got this right early.\u003C\u002Fp>",{"id":233,"html":234,"type":138},"b29","\u003C!-- quiz:start -->",{"id":236,"html":237,"text":237,"type":149,"level":31},"b30","🧠 Test yourself",{"id":239,"html":240,"type":138},"b31","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnavigation-api-intercept-navigation\u002Fquiz\">Take the 8-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":242,"html":243,"type":138},"b32","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":245,"html":246,"type":138},"b33","\u003C!-- quiz:end -->",{"id":248,"type":249},"b34","divider",{"id":251,"html":252,"type":138},"b35","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":254,"html":255,"type":138},"b36","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":257,"type":194,"items":258,"ordered":18},"b37",[259,260,261],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","Click a link. Before the new page finishes loading, click a different one. You'd expect the second click to win — that's what happens with a plain `\u003Ca href>` and a full page load. In a lot of single-page apps, it doesn't.\n\nWhat actually happens: the first navigation starts fetching data. The second navigation starts too. Whichever fetch resolves last wins, regardless of which link you clicked last. For a second, the URL bar says one page and the rendered content says another. On a slow connection, \"for a second\" can be long enough for someone to screenshot it and file a bug titled \"app shows wrong page.\"\n\nHere's the part that surprises people: this isn't a bug in your router. It's the shape of the tool.\n\n## How `pushState` routing actually works\n\nEvery client-side router — the ones you've built by hand and the ones bundled into frameworks — is doing roughly this:\n\n```js\ndocument.addEventListener(\"click\", (e) => {\n  const link = e.target.closest(\"a\");\n  if (!link || !isInternal(link)) return;\n\n  e.preventDefault();\n  history.pushState({}, \"\", link.href);\n  renderRoute(link.href); \u002F\u002F fetch data, swap the view\n});\n\nwindow.addEventListener(\"popstate\", () => {\n  renderRoute(location.href); \u002F\u002F back\u002Fforward button\n});\n```\n\nRead that again and notice what's missing: nothing here knows about the *previous* `renderRoute` call while a new one starts. `pushState` already changed the URL by the time `renderRoute` begins — the navigation, as far as the browser's history is concerned, already happened. Your router isn't intercepting it. It's reacting to something already committed, then racing its own async work against whatever the last reaction was doing.\n\nThat's the double-click bug in one sentence: two `renderRoute` calls, no relationship between them, last one to finish wins. Add a canceled `fetch` and an `AbortController` to the mix and you can make it *mostly* go away — mature router libraries do exactly that. But you're patching a race condition from outside the event that caused it, because the click handler and the URL change were never one atomic thing to begin with.\n\n## The API built to fire before the URL changes\n\nThe [Navigation API](https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FNavigation_API) exists specifically to close that gap. Instead of listening for `popstate` after a change, you listen for `navigate` on `window.navigation` — and that event fires *before* the browser commits to the new URL, for every kind of navigation: link clicks, back\u002Fforward, `history.pushState`, even form submissions.\n\n```js\nnavigation.addEventListener(\"navigate\", (event) => {\n  if (!event.canIntercept) return; \u002F\u002F e.g. cross-origin navigations can't be\n\n  const url = new URL(event.destination.url);\n\n  event.intercept({\n    async handler() {\n      await renderRoute(url); \u002F\u002F the browser waits on this\n    },\n  });\n});\n```\n\n`intercept()` is the piece `popstate` never had: it hands the browser a promise and the browser *waits on it* before treating the navigation as finished — no spinner state you have to fake, no separate \"is this route still current\" flag to check by hand. And because every `navigate` event carries its own `event.destination`, a second click firing a second `navigate` event doesn't need to guess whether an earlier one is still in flight — you can call `event.signal` (an `AbortSignal` the browser aborts automatically when a newer navigation supersedes this one) inside your handler and pass it straight to `fetch`:\n\n```js\nevent.intercept({\n  async handler() {\n    const data = await fetch(url, { signal: event.signal }).then(r => r.json());\n    render(data);\n  },\n});\n```\n\nClick a second link before the first `handler` resolves, and the browser aborts the first navigation's signal for you. The stale fetch throws, you skip rendering it, and there's no manual bookkeeping to get wrong — the race condition isn't patched, it's structurally not possible to hit.\n\n## What you get that `pushState` never gave you\n\nA few things fall out of treating navigation as one interceptable event instead of a click handler plus a `popstate` listener bolted on the side:\n\n- **One event for everything.** Link clicks, back\u002Fforward, and programmatic `navigation.navigate()` calls all go through the same `navigate` event — you're not maintaining a click handler and a separate `popstate` handler that have to agree with each other.\n- **Cancellation that's actually cancellation.** `event.preventDefault()` on `navigate` stops the navigation before the URL changes, not after — useful for \"you have unsaved changes\" guards that used to require faking the URL back with another `pushState` call.\n- **Real navigation state.** `navigation.currentEntry`, `navigation.entries()`, and events like `navigatesuccess` \u002F `navigateerror` give you a queryable list of history entries with keys and state, instead of `history.length` (a number that tells you nothing about *what's* in the stack).\n\nNone of this replaces routing logic — you still decide what URL maps to what view. It replaces the part where the browser's actual navigation and your app's idea of navigation are two loosely-synced systems held together by `preventDefault` and hope.\n\n## Where it stands today\n\nThe Navigation API has shipped in Chrome and Edge since 2022. Safari and Firefox don't have it yet as of this writing, which is the honest caveat: this isn't a drop-in replacement for your router today, and any production use needs a fallback path (or a router library that already detects and uses it under the hood, falling back to `pushState` where it's missing). Check current support before reaching for it directly in anything beyond a Chromium-only tool.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnavigation-api-intercept-navigation\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## The one thing worth remembering\n\n`popstate` tells you a navigation happened. `navigate` tells you one is *about* to, and lets you decide what \"happened\" even means. That difference is the whole reason the double-click race exists in the first place — and the whole reason it stops existing once the browser is holding the promise instead of you holding a flag.\n\nNext time your router's navigation logic starts growing `isNavigating` booleans and manual `AbortController` bookkeeping, that's usually this gap showing through. Have you hit the double-navigation race in production, or did your router already paper over it? I'd like to know which libraries got this right early.\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 8-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnavigation-api-intercept-navigation\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)",{"title":109,"canonical":264,"description":265},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnavigation-api-intercept-navigation","Double-click a link during a slow route change and most SPA routers render two pages and settle on the wrong one — because they're listening for navigation after it already happene","01a04446-4fc4-73e1-b763-f0ba9986a883",{"id":268,"locked":18},"01a04446-4ff0-7073-9920-53a4653074a5",[270],{"id":45,"slug":46,"title":48,"_count":271},{"questions":51},[273],{"locale":13,"slug":46},{"id":45,"slug":46,"title":48,"_count":275,"questionCount":51},{"questions":51}]