[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"article-navigation-api-intercept-navigation":44,"search-suggestions":225,"related-navigation-api-intercept-navigation":265,"code:js:true:123opst":398,"code:js:true:1ghcndp":399,"code:js:true:im4red":400,"comments-01a04446-4fc4-73e1-b763-eda8c9cf631f":401},[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,"title":47,"subtitle":48,"excerpt":49,"coverUrl":50,"locale":13,"readingMinutes":51,"publishedAt":52,"viewCount":53,"likeCount":19,"commentCount":19,"author":54,"vertical":59,"topic":60,"tags":63,"_count":75,"playground":77,"body":79,"bodyMd":208,"seo":209,"translationGroupId":212,"series":48,"podcastUrl":48,"verticalId":5,"thread":213,"assessments":215,"translations":221,"quiz":223},"01a04446-4fc4-73e1-b763-eda8c9cf631f","navigation-api-intercept-navigation","Your Router Doesn't Intercept Navigation. It Reacts To It.",null,"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",5,"2026-09-01T11:23:43.875Z",30,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},"javascript","JavaScript",[64,66,69,72],{"slug":61,"name":65,"color":48},"Javascript",{"slug":67,"name":68,"color":48},"webdev","Webdev",{"slug":70,"name":71,"color":48},"browser","Browser",{"slug":73,"name":74,"color":48},"frontend","Frontend",{"assessments":76},1,{"slug":46,"title":78},"Navigation race — interactive playground",{"blocks":80,"version":76},[81,85,88,91,96,99,105,108,111,114,117,121,124,128,131,135,138,145,148,151,154,157,160,163,166,169,172,175,178,181,184,187,190,193,196,199,202],{"id":82,"html":83,"type":84},"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":86,"html":87,"type":84},"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":89,"html":90,"type":84},"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":92,"html":93,"text":94,"type":95,"level":31},"b4","How \u003Ccode>pushState\u003C\u002Fcode> routing actually works","How pushState routing actually works","heading",{"id":97,"html":98,"type":84},"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":100,"code":101,"type":102,"language":103,"highlight":104},"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":106,"html":107,"type":84},"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":109,"html":110,"type":84},"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":112,"html":113,"text":113,"type":95,"level":31},"b9","The API built to fire before the URL changes",{"id":115,"html":116,"type":84},"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":118,"code":119,"type":102,"language":103,"highlight":120},"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":122,"html":123,"type":84},"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":125,"code":126,"type":102,"language":103,"highlight":127},"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":129,"html":130,"type":84},"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":132,"html":133,"text":134,"type":95,"level":31},"b15","What you get that \u003Ccode>pushState\u003C\u002Fcode> never gave you","What you get that pushState never gave you",{"id":136,"html":137,"type":84},"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":139,"type":140,"items":141,"ordered":18},"b17","list",[142,143,144],"\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":146,"html":147,"type":84},"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":149,"html":150,"text":150,"type":95,"level":31},"b19","Where it stands today",{"id":152,"html":153,"type":84},"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":155,"html":156,"type":84},"b21","\u003C!-- playground:start -->",{"id":158,"html":159,"text":159,"type":95,"level":31},"b22","🎮 Try it yourself",{"id":161,"html":162,"type":84},"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":164,"html":165,"type":84},"b24","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":167,"html":168,"type":84},"b25","\u003C!-- playground:end -->",{"id":170,"html":171,"text":171,"type":95,"level":31},"b26","The one thing worth remembering",{"id":173,"html":174,"type":84},"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":176,"html":177,"type":84},"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":179,"html":180,"type":84},"b29","\u003C!-- quiz:start -->",{"id":182,"html":183,"text":183,"type":95,"level":31},"b30","🧠 Test yourself",{"id":185,"html":186,"type":84},"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":188,"html":189,"type":84},"b32","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":191,"html":192,"type":84},"b33","\u003C!-- quiz:end -->",{"id":194,"type":195},"b34","divider",{"id":197,"html":198,"type":84},"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":200,"html":201,"type":84},"b36","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":203,"type":140,"items":204,"ordered":18},"b37",[205,206,207],"⭐ \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":47,"canonical":210,"description":211},"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":214,"locked":18},"01a04446-4ff0-7073-9920-53a4653074a5",[216],{"id":217,"slug":46,"title":218,"_count":219},"01a04446-501a-7095-8534-775f8a6d3c76","Navigation API: intercept before it commits",{"questions":220},8,[222],{"locale":13,"slug":46},{"id":217,"slug":46,"title":218,"_count":224,"questionCount":220},{"questions":220},[226,228,230,232,236,240,244,248,251,255,258,262],{"slug":67,"name":68,"articles":227},77,{"slug":73,"name":74,"articles":229},65,{"slug":61,"name":65,"articles":231},64,{"slug":233,"name":234,"articles":235},"css","Css",29,{"slug":237,"name":238,"articles":239},"tutorial","Tutorial",16,{"slug":241,"name":242,"articles":243},"typescript","Typescript",11,{"slug":245,"name":246,"articles":247},"performance","Performance",10,{"slug":249,"name":250,"articles":220},"react","React",{"slug":252,"name":253,"articles":254},"grammar","Grammar",6,{"slug":256,"name":257,"articles":51},"node","Node",{"slug":259,"name":260,"articles":261},"programming","Programming",4,{"slug":263,"name":264,"articles":261},"html","Html",{"items":266,"meta":396},[267,279,298,318,340,359,377],{"id":45,"slug":46,"title":47,"subtitle":48,"excerpt":49,"coverUrl":50,"locale":13,"readingMinutes":51,"publishedAt":52,"viewCount":268,"likeCount":19,"commentCount":19,"author":269,"vertical":270,"topic":271,"tags":272,"_count":277,"playground":278,"hasQuiz":17,"hasPlayground":17},31,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[273,274,275,276],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":70,"name":71,"color":48},{"slug":73,"name":74,"color":48},{"assessments":76},{"slug":46},{"id":280,"slug":281,"title":282,"subtitle":48,"excerpt":283,"coverUrl":284,"locale":13,"readingMinutes":285,"publishedAt":286,"viewCount":287,"likeCount":19,"commentCount":19,"author":288,"vertical":289,"topic":290,"tags":291,"_count":296,"playground":297,"hasQuiz":17,"hasPlayground":17},"019ff73a-fb5b-73ce-b725-2912167a7dde","weekly-debounce-and-throttle","Debounce and Throttle in JavaScript: The Complete Guide","Learn debounce and throttle in JavaScript step by step, with worked examples, edge cases, and a copy-paste cheat sheet for search, scroll, and resize.","\u002Fmedia\u002Fcovers\u002Fweekly-debounce-and-throttle.png",14,"2026-08-31T13:12:14.139Z",79,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[292,293,294,295],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":237,"name":238,"color":48},{"slug":245,"name":246,"color":48},{"assessments":76},{"slug":281},{"id":299,"slug":300,"title":301,"subtitle":48,"excerpt":302,"coverUrl":303,"locale":13,"readingMinutes":254,"publishedAt":304,"viewCount":305,"likeCount":19,"commentCount":19,"author":306,"vertical":307,"topic":308,"tags":309,"_count":316,"playground":317,"hasQuiz":17,"hasPlayground":17},"01a03cdb-3743-73ef-ab9d-21f03158ca35","server-sent-events-eventsource-live-updates","You Don't Need a WebSocket for That Live Feed","A live notification badge, a progress bar, a dashboard counter — you reached for socket.io and started hand-rolling reconnect logic. The browser already ships a simpler API that does it for you.","\u002Fmedia\u002Fcovers\u002Fserver-sent-events-eventsource-live-updates.png","2026-08-31T13:11:42.385Z",47,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[310,311,312,313],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":237,"name":238,"color":48},{"slug":314,"name":315,"color":48},"apis","Apis",{"assessments":76},{"slug":300},{"id":319,"slug":320,"title":321,"subtitle":48,"excerpt":322,"coverUrl":323,"locale":13,"readingMinutes":285,"publishedAt":324,"viewCount":325,"likeCount":19,"commentCount":19,"author":326,"vertical":327,"topic":328,"tags":331,"_count":338,"playground":339,"hasQuiz":17,"hasPlayground":17},"01a04446-3f08-701c-8cdc-e5ab5e87086e","nuxt-weekly-cross-request-state-leak","Nuxt useState vs ref(): Why Server State Leaks Across Users","A module-scope ref() in Nuxt is shared by every request that hits your server. Learn why useState isolates state per user, and how to fix the leak.","\u002Fmedia\u002Fcovers\u002Fnuxt-weekly-cross-request-state-leak.png","2026-08-30T11:37:11.335Z",106,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":329,"name":330},"nuxt","Nuxt",[332,333,336,337],{"slug":329,"name":330,"color":48},{"slug":334,"name":335,"color":48},"ssr","SSR",{"slug":61,"name":65,"color":48},{"slug":237,"name":238,"color":48},{"assessments":76},{"slug":320},{"id":341,"slug":342,"title":343,"subtitle":48,"excerpt":344,"coverUrl":345,"locale":13,"readingMinutes":346,"publishedAt":347,"viewCount":348,"likeCount":19,"commentCount":19,"author":349,"vertical":350,"topic":351,"tags":352,"_count":357,"playground":358,"hasQuiz":17,"hasPlayground":17},"01a04446-373a-775b-8aaf-b72c8952123d","react-weekly-rerender-vs-remount","React Re-render vs Remount: What Actually Triggers Each","A practical guide to React re-render vs remount: what type, position, and key decide, why state resets unexpectedly, and how to force a remount.","\u002Fmedia\u002Fcovers\u002Freact-weekly-rerender-vs-remount.png",13,"2026-08-29T12:32:41.200Z",164,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":249,"name":250},[353,354,355,356],{"slug":249,"name":250,"color":48},{"slug":61,"name":65,"color":48},{"slug":237,"name":238,"color":48},{"slug":67,"name":68,"color":48},{"assessments":76},{"slug":342},{"id":360,"slug":361,"title":362,"subtitle":48,"excerpt":363,"coverUrl":364,"locale":13,"readingMinutes":51,"publishedAt":365,"viewCount":366,"likeCount":19,"commentCount":19,"author":367,"vertical":368,"topic":369,"tags":370,"_count":375,"playground":376,"hasQuiz":17,"hasPlayground":17},"01a02839-480f-7428-914a-2c0442d00855","intl-segmenter-grapheme-word-sentence","You're truncating bios with `.slice()`. `Intl.Segmenter` knows where the emoji actually end.","A 30-character bio limit that cuts off mid-emoji isn't a rendering bug — it's .length counting UTF-16 code units instead of what's on screen. Intl.Segmenter counts graphemes, words, and sentences the way a reader actually sees them, and every major browser supports it now.","\u002Fmedia\u002Fcovers\u002Fintl-segmenter-grapheme-word-sentence.png","2026-08-27T17:30:57.707Z",206,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[371,372,373,374],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":73,"name":74,"color":48},{"slug":237,"name":238,"color":48},{"assessments":76},{"slug":361},{"id":378,"slug":379,"title":380,"subtitle":48,"excerpt":381,"coverUrl":382,"locale":13,"readingMinutes":51,"publishedAt":383,"viewCount":384,"likeCount":19,"commentCount":19,"author":385,"vertical":386,"topic":387,"tags":389,"_count":394,"playground":395,"hasQuiz":17,"hasPlayground":17},"01a02319-4485-7310-b494-dc94bdbfb49c","css-field-sizing-auto-grow-textarea","Delete Your Auto-Resize Textarea JS. CSS Does It Now.","The scrollHeight hack everyone copy-pastes for auto-growing textareas has a one-line CSS replacement — plus the one gotcha that bites if you skip max-height.","\u002Fmedia\u002Fcovers\u002Fcss-field-sizing-auto-grow-textarea.png","2026-08-26T06:56:38.974Z",223,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":233,"name":388},"CSS",[390,391,392,393],{"slug":233,"name":234,"color":48},{"slug":67,"name":68,"color":48},{"slug":73,"name":74,"color":48},{"slug":61,"name":65,"color":48},{"assessments":76},{"slug":379},{"page":76,"perPage":397,"total":231,"totalPages":247},7,"\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">document\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">addEventListener\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"click\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>e\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> link \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> e\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>target\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">closest\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"a\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">!\u003C\u002Fspan>link \u003Cspan class=\"shj-oper\">||\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">!\u003C\u002Fspan>\u003Cspan class=\"shj-func\">isInternal\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>link\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan>;\n\n  e\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">preventDefault\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  history\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">pushState\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> link\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>href\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-func\">renderRoute\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>link\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>href\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F fetch data, swap the view\n\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\nwindow\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">addEventListener\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"popstate\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-func\">renderRoute\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>location\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>href\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F back\u002Fforward button\n\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">navigation\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">addEventListener\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"navigate\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>event\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">!\u003C\u002Fspan>event\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>canIntercept\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F e.g. cross-origin navigations can't be\n\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> url \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-num\">URL\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>event\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>destination\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>url\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\n  event\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">intercept\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-func\">handler\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n      \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> \u003Cspan class=\"shj-func\">renderRoute\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>url\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F the browser waits on this\n\u003C\u002Fspan>    \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">event\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">intercept\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-func\">handler\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> data \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> \u003Cspan class=\"shj-func\">fetch\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>url\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> signal\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> event\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>signal \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">then\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>r \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> r\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">json\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n    \u003Cspan class=\"shj-func\">render\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>data\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>",{"locked":18,"total":19,"comments":402},[]]