[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"quiz-nuxt-weekly-useasyncdata-keys-dedupe":44,"search-suggestions":60,"quiz-article-nuxt-weekly-useasyncdata-keys-dedupe":105},[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},"01a08c59-46d0-746c-ac04-5fcfd7f8521a","nuxt-weekly-useasyncdata-keys-dedupe","PRACTICE_QUIZ","useAsyncData Keys, Caching & Dedupe","Test what you learned about how Nuxt keys useAsyncData\u002FuseFetch calls, why wrapper composables can silently collide, and how dedupe controls in-flight requests.",{"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,72,76,80,84,88,92,95,98,102],{"slug":62,"name":63,"articles":64},"webdev","Webdev",93,{"slug":66,"name":67,"articles":68},"javascript","Javascript",78,{"slug":70,"name":71,"articles":53},"frontend","Frontend",{"slug":73,"name":74,"articles":75},"css","Css",32,{"slug":77,"name":78,"articles":79},"tutorial","Tutorial",24,{"slug":81,"name":82,"articles":83},"typescript","Typescript",14,{"slug":85,"name":86,"articles":87},"performance","Performance",12,{"slug":89,"name":90,"articles":91},"react","React",11,{"slug":93,"name":94,"articles":51},"browser","Browser",{"slug":96,"name":97,"articles":51},"node","Node",{"slug":99,"name":100,"articles":101},"grammar","Grammar",6,{"slug":103,"name":104,"articles":101},"programming","Programming",{"id":106,"slug":46,"title":107,"subtitle":52,"excerpt":108,"coverUrl":109,"locale":13,"readingMinutes":83,"publishedAt":110,"viewCount":68,"likeCount":19,"commentCount":19,"author":111,"vertical":116,"topic":117,"tags":120,"_count":127,"playground":129,"body":131,"bodyMd":475,"seo":476,"translationGroupId":478,"series":479,"podcastUrl":52,"verticalId":5,"thread":487,"assessments":489,"translations":492,"quiz":494},"01a08c59-464c-7039-acf3-b32a0b9ee7ce","useAsyncData Keys in Nuxt: Caching, Dedupe & the Sharing Bug","Learn how Nuxt's useAsyncData and useFetch generate cache keys, how dedupe (cancel vs defer) really works, and why wrapper composables silently share data.","\u002Fmedia\u002Fcovers\u002Fnuxt-weekly-useasyncdata-keys-dedupe.png","2026-09-13T11:26:28.014Z",{"id":112,"name":113,"username":114,"avatarUrl":52,"headline":115},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":118,"name":119},"nuxt","Nuxt",[121,122,125,126],{"slug":118,"name":119,"color":52},{"slug":123,"name":124,"color":52},"ssr","SSR",{"slug":66,"name":67,"color":52},{"slug":77,"name":78,"color":52},{"assessments":128},1,{"slug":46,"title":130},"useAsyncData key collision — interactive simulation",{"blocks":132,"version":128},[133,137,140,145,148,157,160,163,166,169,184,187,190,196,199,204,207,210,214,217,220,223,226,229,233,236,240,243,247,250,254,257,260,263,268,272,276,279,282,285,288,291,294,297,301,308,311,314,322,325,333,336,339,342,345,348,351,354,357,360,363,366,369,413,416,424,427,430,433,436,439,442,445,448,451,454,457,460,463,466,469],{"id":134,"html":135,"type":136},"b1","\u003Cp>Two product cards on the same page, fetched with two different IDs, and both show product #1&#39;s data. No error in the console, no failed request in the network tab — \u003Ccode>useAsyncData\u003C\u002Fcode> did exactly what you asked it to do. The bug is that you didn&#39;t ask it what you thought you did.\u003C\u002Fp>","paragraph",{"id":138,"html":139,"type":136},"b2","\u003Cp>This is one of the most common &quot;it works everywhere except in this one wrapper&quot; bugs in Nuxt, and it comes from a single, easy-to-miss fact: \u003Ccode>useAsyncData\u003C\u002Fcode>&#39;s cache key is not derived from the data you&#39;re fetching — it&#39;s derived from \u003Cem>where in your source code you call it\u003C\u002Fem>.\u003C\u002Fp>",{"id":141,"html":142,"text":143,"type":144,"level":31},"b3","What you&#39;ll learn","What you'll learn","heading",{"id":146,"html":147,"type":136},"b4","\u003Cp>By the end of this article you&#39;ll be able to:\u003C\u002Fp>",{"id":149,"type":150,"items":151,"ordered":18},"b5","list",[152,153,154,155,156],"Explain how \u003Ccode>useAsyncData\u003C\u002Fcode> and \u003Ccode>useFetch\u003C\u002Fcode> generate a cache key when you don&#39;t supply one, and why that breaks inside a wrapper composable","Read and choose between \u003Ccode>dedupe: &#39;cancel&#39;\u003C\u002Fcode> and \u003Ccode>dedupe: &#39;defer&#39;\u003C\u002Fcode> for a given piece of UI","Trace how a key connects a server-rendered fetch to the payload the client hydrates from","Use \u003Ccode>refresh()\u003C\u002Fcode>, \u003Ccode>clearNuxtData()\u003C\u002Fcode>, and the \u003Ccode>watch\u003C\u002Fcode> option to control exactly when a re-fetch happens","Give every dynamic fetch an explicit, correct key, on reflex",{"id":158,"html":159,"text":159,"type":144,"level":31},"b6","Who this is for",{"id":161,"html":162,"type":136},"b7","\u003Cp>You&#39;ve used \u003Ccode>useAsyncData\u003C\u002Fcode> or \u003Ccode>useFetch\u003C\u002Fcode> in a Nuxt page or component at least once. You don&#39;t need to have hit this bug yet — the article builds the mental model from the first principles of what a &quot;key&quot; is for.\u003C\u002Fp>",{"id":164,"html":165,"type":136},"b8","\u003Cp>This article is written against \u003Cstrong>Nuxt 4.5.x\u003C\u002Fstrong> (verified against the \u003Ccode>nuxt\u003C\u002Fcode> package&#39;s npm dist-tags in September 2026; the latest patch at the time of writing is \u003Cstrong>4.5.2\u003C\u002Fstrong>, with Nuxt 3 having reached end-of-life on July 31, 2026). Code and directory paths use the Nuxt 4 \u003Ccode>app\u002F\u003C\u002Fcode> convention (\u003Ccode>app\u002Fcomposables\u002F\u003C\u002Fcode>, \u003Ccode>app\u002Fpages\u002F\u003C\u002Fcode>); if your project still uses the flat Nuxt 3 layout (\u003Ccode>composables\u002F\u003C\u002Fcode>, \u003Ccode>pages\u002F\u003C\u002Fcode> at the root), everything below works unchanged — only the folder location differs.\u003C\u002Fp>",{"id":167,"html":168,"text":168,"type":144,"level":31},"b9","Table of contents",{"id":170,"type":150,"items":171,"ordered":18},"b10",[172,173,174,175,176,177,178,179,180,181,182,183],"\u003Ca href=\"#the-problem-two-products-one-row-of-data\">The problem: two products, one row of data\u003C\u002Fa>","\u003Ca href=\"#the-mental-model-a-keys-identity-not-a-label\">The mental model: a key is the fetch&#39;s identity, not a label\u003C\u002Fa>","\u003Ca href=\"#stage-1-the-minimal-useasyncdata-usefetch-call\">Stage 1: the minimal useAsyncData \u002F useFetch call\u003C\u002Fa>","\u003Ca href=\"#stage-2-what-happens-when-you-dont-pass-a-key\">Stage 2: what happens when you don&#39;t pass a key\u003C\u002Fa>","\u003Ca href=\"#stage-3-dedupe-cancel-vs-defer\">Stage 3: dedupe — cancel vs defer\u003C\u002Fa>","\u003Ca href=\"#stage-4-how-the-key-connects-server-and-client\">Stage 4: how the key connects server and client\u003C\u002Fa>","\u003Ca href=\"#stage-5-refreshing-clearing-and-watch\">Stage 5: refreshing, clearing, and watch\u003C\u002Fa>","\u003Ca href=\"#edge-cases-and-gotchas\">Edge cases and gotchas\u003C\u002Fa>","\u003Ca href=\"#best-practices-keys-and-dedupe-you-can-trust\">Best practices: keys and dedupe you can trust\u003C\u002Fa>","\u003Ca href=\"#faq\">FAQ\u003C\u002Fa>","\u003Ca href=\"#cheat-sheet\">Cheat sheet\u003C\u002Fa>","\u003Ca href=\"#key-takeaways\">Key takeaways\u003C\u002Fa>",{"id":185,"html":186,"text":186,"type":144,"level":31},"b11","The problem: two products, one row of data",{"id":188,"html":189,"type":136},"b12","\u003Cp>Say you build a small wrapper composable to keep your components clean:\u003C\u002Fp>",{"id":191,"code":192,"type":193,"language":194,"highlight":195},"b13","\u002F\u002F app\u002Fcomposables\u002FuseProduct.ts\nexport function useProduct(id: Ref\u003Cstring> | string) {\n  return useAsyncData(() => $fetch(`\u002Fapi\u002Fproducts\u002F${unref(id)}`))\n  \u002F\u002F                    ^ no key passed — looks harmless\n}","code","ts",[],{"id":197,"html":198,"type":136},"b14","\u003Cp>And you use it twice on the same page:\u003C\u002Fp>",{"id":200,"code":201,"type":193,"language":202,"highlight":203},"b15","\u003Cscript setup lang=\"ts\">\nconst { data: featured } = useProduct('sku-101')\nconst { data: related } = useProduct('sku-204')\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003CProductCard :product=\"featured\" \u002F>\n  \u003CProductCard :product=\"related\" \u002F>\n  \u003C!-- both cards render sku-101 -->\n\u003C\u002Ftemplate>","vue",[],{"id":205,"html":206,"type":136},"b16","\u003Cp>Both \u003Ccode>&lt;ProductCard&gt;\u003C\u002Fcode>s show the same product. There&#39;s no thrown error, because nothing went wrong at the network level — both requests may even have fired correctly. The bug is upstream of the network: Nuxt handed both calls the \u003Cem>same\u003C\u002Fem> cache entry, so the second call&#39;s result overwrote (or was skipped in favor of) the first&#39;s.\u003C\u002Fp>",{"id":208,"html":209,"type":136},"b17","\u003Cp>The instinct here is to blame \u003Ccode>$fetch\u003C\u002Fcode>, the API, or a race condition. It&#39;s none of those. It&#39;s the key.\u003C\u002Fp>",{"id":211,"html":212,"text":213,"type":144,"level":31},"b18","The mental model: a key is the fetch&#39;s identity, not a label","The mental model: a key is the fetch's identity, not a label",{"id":215,"html":216,"type":136},"b19","\u003Cp>\u003Cstrong>The mental model:\u003C\u002Fstrong> every \u003Ccode>useAsyncData\u003C\u002Fcode> (and \u003Ccode>useFetch\u003C\u002Fcode>, which is \u003Ccode>useAsyncData\u003C\u002Fcode> with the URL folded in) call is backed by an entry in a shared, page-level store keyed by a string. Two calls with the \u003Cem>same key\u003C\u002Fem> are, as far as Nuxt is concerned, \u003Cem>the same fetch\u003C\u002Fem> — they share one \u003Ccode>data\u003C\u002Fcode> ref, one \u003Ccode>pending\u003C\u002Fcode> ref, one \u003Ccode>error\u003C\u002Fcode> ref, and one in-flight request. Two calls with \u003Cem>different keys\u003C\u002Fem> are entirely unrelated, even if they happen to call the exact same URL.\u003C\u002Fp>",{"id":218,"html":219,"type":136},"b20","\u003Cp>The key is not a label you attach to data you already have. It&#39;s how Nuxt decides whether two calls in your app are \u003Cem>asking for the same thing\u003C\u002Fem>. If you don&#39;t supply one, Nuxt has to invent one — and it does that with a build-time compiler step that reads the \u003Cstrong>file and line number\u003C\u002Fstrong> where \u003Ccode>useAsyncData()\u003C\u002Fcode> is written in your source, and turns that location into a deterministic auto-key.\u003C\u002Fp>",{"id":221,"html":222,"type":136},"b21","\u003Cp>That&#39;s the trap: the compiler sees the call site inside \u003Ccode>useProduct.ts\u003C\u002Fcode>, not inside your page. \u003Ccode>useProduct(&#39;sku-101&#39;)\u003C\u002Fcode> and \u003Ccode>useProduct(&#39;sku-204&#39;)\u003C\u002Fcode> both resolve to a call to \u003Ccode>useAsyncData()\u003C\u002Fcode> on the \u003Cem>same line of the same file\u003C\u002Fem> — the body of the wrapper. So both get the same auto-generated key, regardless of which ID was passed in. The wrapper hid the one piece of information Nuxt needed to tell the two calls apart.\u003C\u002Fp>",{"id":224,"html":225,"text":225,"type":144,"level":31},"b22","Stage 1: the minimal useAsyncData \u002F useFetch call",{"id":227,"html":228,"type":136},"b23","\u003Cp>Written directly in a page or component (no wrapper), an explicit key removes all ambiguity:\u003C\u002Fp>",{"id":230,"code":231,"type":193,"language":202,"highlight":232},"b24","\u003Cscript setup lang=\"ts\">\nconst route = useRoute()\n\n\u002F\u002F Key concept: the key says \"this is product-\u003Cid>\", nothing else does.\nconst { data: product, status } = useAsyncData(\n  `product-${route.params.id}`,\n  () => $fetch(`\u002Fapi\u002Fproducts\u002F${route.params.id}`)\n)\n\u003C\u002Fscript>",[],{"id":234,"html":235,"type":136},"b25","\u003Cp>\u003Ccode>useFetch\u003C\u002Fcode> is the shorthand for the common case — a call that&#39;s really just &quot;GET this URL, reactively&quot;:\u003C\u002Fp>",{"id":237,"code":238,"type":193,"language":202,"highlight":239},"b26","\u003Cscript setup lang=\"ts\">\nconst route = useRoute()\n\n\u002F\u002F useFetch derives its own key from the method, URL, and reactive options —\n\u002F\u002F you rarely need to pass one explicitly here, because the URL already\n\u002F\u002F carries the id.\nconst { data: product } = useFetch(() => `\u002Fapi\u002Fproducts\u002F${route.params.id}`)\n\u003C\u002Fscript>",[],{"id":241,"html":242,"type":136},"b27","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> \u003Ccode>useFetch\u003C\u002Fcode>&#39;s implicit key already bakes in the URL, so a \u003Cem>reactive\u003C\u002Fem> URL (an arrow function, as above) naturally produces a different key per product. That single detail is why \u003Ccode>useFetch\u003C\u002Fcode> feels safer than \u003Ccode>useAsyncData\u003C\u002Fcode> by default — its auto-key is derived from something that actually varies with your data, not from source position.\u003C\u002Fp>",{"id":244,"html":245,"text":246,"type":144,"level":31},"b28","Stage 2: what happens when you don&#39;t pass a key","Stage 2: what happens when you don't pass a key",{"id":248,"html":249,"type":136},"b29","\u003Cp>Call \u003Ccode>useAsyncData\u003C\u002Fcode> directly in a page, with no wrapper in between, and the file-and-line auto-key works fine — every distinct call site in your codebase is, by definition, a distinct line. The trap only appears once you factor a \u003Ccode>useAsyncData\u003C\u002Fcode> call into a \u003Cstrong>shared function\u003C\u002Fstrong> that different callers invoke with different arguments — exactly the &quot;let&#39;s DRY this up into a composable&quot; instinct that Vue and Nuxt otherwise reward.\u003C\u002Fp>",{"id":251,"code":252,"type":193,"language":194,"highlight":253},"b30","\u002F\u002F app\u002Fcomposables\u002FuseProduct.ts — the version that actually works\nexport function useProduct(id: Ref\u003Cstring> | string) {\n  return useAsyncData(\n    `product-${unref(id)}`,           \u002F\u002F key now varies with the argument\n    () => $fetch(`\u002Fapi\u002Fproducts\u002F${unref(id)}`)\n  )\n}",[],{"id":255,"html":256,"type":136},"b31","\u003Cp>The fix is one line: pass a key that&#39;s built from the argument, not the call site. The same rule applies to any composable you write around \u003Ccode>useAsyncData\u003C\u002Fcode>, \u003Ccode>useLazyAsyncData\u003C\u002Fcode>, or a hand-rolled data-fetching hook — \u003Cstrong>the moment a \u003Ccode>useAsyncData\u003C\u002Fcode> call is wrapped in a function that more than one place will call with different inputs, that key must be explicit and must include those inputs.\u003C\u002Fstrong>\u003C\u002Fp>",{"id":258,"html":259,"text":259,"type":144,"level":31},"b32","Stage 3: dedupe — cancel vs defer",{"id":261,"html":262,"type":136},"b33","\u003Cp>Keys decide \u003Cem>whether\u003C\u002Fem> two calls are the same fetch. \u003Ccode>dedupe\u003C\u002Fcode> decides what happens when the \u003Cem>same key\u003C\u002Fem> is asked for again while a request for it is still in flight. There are exactly two values, and Nuxt defaults to \u003Ccode>&#39;cancel&#39;\u003C\u002Fcode>:\u003C\u002Fp>",{"id":264,"type":150,"items":265,"ordered":18},"b34",[266,267],"\u003Cstrong>\u003Ccode>dedupe: &#39;cancel&#39;\u003C\u002Fcode>\u003C\u002Fstrong> (the default) — abort the in-flight request for this key and start a fresh one. Correct when only the \u003Cem>latest\u003C\u002Fem> request matters: a search box that refetches on every keystroke, a filter panel, anything where an older in-flight response would be stale by the time it arrives.","\u003Cstrong>\u003Ccode>dedupe: &#39;defer&#39;\u003C\u002Fcode>\u003C\u002Fstrong> — if a request for this key is already in flight, don&#39;t start a second one; the new call reuses the pending request instead. Correct when the request is expensive or has side effects and firing it twice for the same key is simply wasteful (two components in the same render both asking for the same key at the same time, a button a user might double-click).",{"id":269,"code":270,"type":193,"language":194,"highlight":271},"b35","\u002F\u002F A typeahead: only the newest keystroke's result should win.\nconst { data: results } = useAsyncData(\n  () => `search-${query.value}`,\n  () => $fetch('\u002Fapi\u002Fsearch', { query: { q: query.value } }),\n  { watch: [query], dedupe: 'cancel' }\n)",[],{"id":273,"code":274,"type":193,"language":194,"highlight":275},"b36","\u002F\u002F An expensive report two dashboard widgets both need at once:\n\u002F\u002F one request in flight is enough for both.\nconst { data: report } = useAsyncData(\n  'quarterly-report',\n  () => $fetch('\u002Fapi\u002Freports\u002Fquarterly'),\n  { dedupe: 'defer' }\n)",[],{"id":277,"html":278,"type":136},"b37","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> \u003Ccode>dedupe\u003C\u002Fcode> is not debouncing. Debouncing delays \u003Cem>starting\u003C\u002Fem> a call; \u003Ccode>dedupe\u003C\u002Fcode> decides what happens to calls that have \u003Cem>already\u003C\u002Fem> started for the same key. You often want both — debounce the keystroke, then let \u003Ccode>dedupe: &#39;cancel&#39;\u003C\u002Fcode> handle any request that still overlaps.\u003C\u002Fp>",{"id":280,"html":281,"text":281,"type":144,"level":31},"b38","Stage 4: how the key connects server and client",{"id":283,"html":284,"type":136},"b39","\u003Cp>During SSR, Nuxt resolves your page&#39;s \u003Ccode>useAsyncData\u003C\u002Fcode>\u002F\u003Ccode>useFetch\u003C\u002Fcode> calls and serializes the results into a \u003Cstrong>payload\u003C\u002Fstrong> — a plain-data snapshot sent to the browser alongside the HTML, keyed by exactly the same strings you&#39;ve been reading about. On the client, hydration doesn&#39;t re-run your fetches from scratch: for each key, Nuxt&#39;s \u003Ccode>getCachedData\u003C\u002Fcode> step looks in that payload first. If the key is present, the cached value is used immediately and the handler function never runs on the client at all.\u003C\u002Fp>",{"id":286,"html":287,"type":136},"b40","\u003Cp>This is the other reason a duplicated auto-key is dangerous, not just cosmetically wrong: it means the \u003Cem>server\u003C\u002Fem> also only ran one fetch for what you thought were two different pieces of data, so the payload only ever contained product #1&#39;s response in the first place. The bug isn&#39;t purely a client-side rendering glitch — the wrong data was fetched once, on the server, and faithfully shipped to the browser.\u003C\u002Fp>",{"id":289,"html":290,"type":136},"b41","\u003Cp>\u003Cem>(If server-side state feels shaky in general, the previous episode in this series, \u003Ca href=\"https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fnuxt-usestate-vs-ref-why-server-state-leaks-across-users-47n1\">Nuxt useState vs ref(): Why Server State Leaks Across Users\u003C\u002Fa>, covers the sibling bug — module-scope state shared across \u003Cem>requests\u003C\u002Fem> rather than across calls on the same page. Useful background, not required reading.)\u003C\u002Fem>\u003C\u002Fp>",{"id":292,"html":293,"text":293,"type":144,"level":31},"b42","Stage 5: refreshing, clearing, and watch",{"id":295,"html":296,"type":136},"b43","\u003Cp>\u003Ccode>useAsyncData\u003C\u002Fcode> returns more than \u003Ccode>data\u003C\u002Fcode>: \u003Ccode>status\u003C\u002Fcode>, \u003Ccode>pending\u003C\u002Fcode>, \u003Ccode>error\u003C\u002Fcode>, \u003Ccode>refresh\u003C\u002Fcode> (an alias, \u003Ccode>execute\u003C\u002Fcode>, does the same thing), and \u003Ccode>clear\u003C\u002Fcode>.\u003C\u002Fp>",{"id":298,"code":299,"type":193,"language":202,"highlight":300},"b44","\u003Cscript setup lang=\"ts\">\nconst { data: product, refresh, clear } = useAsyncData(\n  `product-${route.params.id}`,\n  () => $fetch(`\u002Fapi\u002Fproducts\u002F${route.params.id}`)\n)\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cbutton @click=\"refresh()\">Reload this product\u003C\u002Fbutton>\n  \u003Cbutton @click=\"clear()\">Reset\u003C\u002Fbutton>\n\u003C\u002Ftemplate>",[],{"id":302,"type":150,"items":303,"ordered":18},"b45",[304,305,306,307],"\u003Cstrong>\u003Ccode>refresh()\u003C\u002Fcode> \u002F \u003Ccode>execute()\u003C\u002Fcode>\u003C\u002Fstrong> — re-runs the handler for this call&#39;s key and updates \u003Ccode>data\u003C\u002Fcode> in place. This is the correct way to force a re-fetch from \u003Cem>inside\u003C\u002Fem> the component that owns the call.","\u003Cstrong>\u003Ccode>clear()\u003C\u002Fcode>\u003C\u002Fstrong> — resets \u003Ccode>data\u003C\u002Fcode> to \u003Ccode>undefined\u003C\u002Fcode> (or the configured default), \u003Ccode>error\u003C\u002Fcode> to \u003Ccode>undefined\u003C\u002Fcode>, and \u003Ccode>status\u003C\u002Fcode> to \u003Ccode>idle\u003C\u002Fcode>, without refetching.","\u003Cstrong>\u003Ccode>refreshNuxtData(key?)\u003C\u002Fcode> \u002F \u003Ccode>clearNuxtData(key?)\u003C\u002Fcode>\u003C\u002Fstrong> — the same two operations, callable from anywhere, by key, when you don&#39;t have a reference to the original composable call (a &quot;save&quot; action in one component that should invalidate a list rendered in another).","\u003Cstrong>\u003Ccode>watch: [...]\u003C\u002Fcode>\u003C\u002Fstrong> — an array of reactive sources; when any of them changes, Nuxt automatically calls \u003Ccode>refresh()\u003C\u002Fcode> for you. This is what makes Stage 3&#39;s search example refetch on every keystroke without a manual \u003Ccode>watch()\u003C\u002Fcode> block of your own.",{"id":309,"html":310,"type":136},"b46","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> changing the \u003Cem>key\u003C\u002Fem> and adding a \u003Cem>watch source\u003C\u002Fem> are two different ways to make a fetch reactive, and they&#39;re not interchangeable. A changing key gives you a distinct cache entry per value (useful when you want to keep old results around, like cached pages of a paginated list). A \u003Ccode>watch\u003C\u002Fcode> source refetches the \u003Cem>same\u003C\u002Fem> entry in place, overwriting it (useful when you only ever care about the current value, like a live search).\u003C\u002Fp>",{"id":312,"html":313,"text":313,"type":144,"level":31},"b47","Edge cases and gotchas",{"id":315,"type":150,"items":316,"ordered":18},"b48",[317,318,319,320,321],"\u003Cstrong>Route params without a param-derived key.\u003C\u002Fstrong> \u003Ccode>useAsyncData(&#39;product&#39;, ...)\u003C\u002Fcode> on a dynamic \u003Ccode>[id].vue\u003C\u002Fcode> page reuses one cache entry across every product route the user navigates to client-side — the classic &quot;stale data flashes for a moment on navigation&quot; bug. Always fold the param into the key.","\u003Cstrong>\u003Ccode>useLazyAsyncData\u003C\u002Fcode> doesn&#39;t block navigation, but it&#39;s the same key machinery.\u003C\u002Fstrong> A lazy call that shares a key with a non-lazy call elsewhere on the page still participates in the same dedupe and cache entry — &quot;lazy&quot; only changes whether Nuxt awaits it before rendering, not how its key behaves.","\u003Cstrong>Two components requesting the same key on the same page is often intentional, not a bug\u003C\u002Fstrong> — it&#39;s exactly how Nuxt avoids two network round-trips for data two widgets both need. The failure mode in this article is the opposite: keys colliding when you \u003Cem>didn&#39;t\u003C\u002Fem> want them to.","\u003Cstrong>\u003Ccode>dedupe: &#39;defer&#39;\u003C\u002Fcode> on a request with side effects can surprise you.\u003C\u002Fstrong> If the &quot;duplicate&quot; call actually needed to trigger a fresh side effect (an analytics ping baked into the handler, say), deferring silently skips it. Keep handlers free of side effects that must run every time they&#39;re called.","\u003Cstrong>SSR + hydration mismatch from a key that differs between server and client.\u003C\u002Fstrong> If your key computation depends on something only available client-side (e.g., \u003Ccode>window\u003C\u002Fcode>-derived state), the server and client will disagree on the key, and the client will refetch instead of hydrating from the payload — usually harmless, but worth knowing so it doesn&#39;t look like a hydration bug elsewhere.",{"id":323,"html":324,"text":324,"type":144,"level":31},"b49","Best practices: keys and dedupe you can trust",{"id":326,"type":150,"items":327,"ordered":18},"b50",[328,329,330,331,332],"\u003Cstrong>Reach for an explicit key\u003C\u002Fstrong> the moment a \u003Ccode>useAsyncData\u003C\u002Fcode> call sits inside a composable, a loop, or anywhere more than one logical &quot;thing&quot; could call it. The rule of thumb: if the data being fetched varies, the key must vary with it.","\u003Cstrong>Build the key from the same values the handler uses\u003C\u002Fstrong> — a template literal with every dynamic input (\u003Ccode>\\\u003C\u002Fcode>product-${id}`\u003Ccode>, not just \u003C\u002Fcode>&#39;product&#39;`) is the whole fix, every time.","\u003Cstrong>Prefer \u003Ccode>useFetch\u003C\u002Fcode>\u003C\u002Fstrong> for the plain &quot;reactively GET this URL&quot; case — its implicit key already includes the URL, so you get the safety of an explicit key for free.","\u003Cstrong>Choose \u003Ccode>dedupe\u003C\u002Fcode> by cost and freshness, not by habit.\u003C\u002Fstrong> Default (\u003Ccode>&#39;cancel&#39;\u003C\u002Fcode>) for anything driven by fast user input where only the latest answer matters; \u003Ccode>&#39;defer&#39;\u003C\u002Fcode> for expensive or side-effect-bearing calls that multiple simultaneous callers can safely share.","\u003Cstrong>Invalidate by key, not by reload.\u003C\u002Fstrong> Reach for \u003Ccode>refresh()\u003C\u002Fcode>\u002F\u003Ccode>refreshNuxtData()\u003C\u002Fcode> and \u003Ccode>clear()\u003C\u002Fcode>\u002F\u003Ccode>clearNuxtData()\u003C\u002Fcode> before reaching for a full page reload or a manually toggled \u003Ccode>key\u003C\u002Fcode> prop to force a remount.",{"id":334,"html":335,"text":335,"type":144,"level":31},"b51","FAQ",{"id":337,"html":338,"text":338,"type":144,"level":43},"b52","Why did my two components end up showing the same data with no error?",{"id":340,"html":341,"type":136},"b53","\u003Cp>Because both \u003Ccode>useAsyncData\u003C\u002Fcode> calls resolved to the same auto-generated key — most often because both went through the same line of a shared wrapper composable. Nuxt didn&#39;t fail; it correctly treated two calls with the same key as one fetch.\u003C\u002Fp>",{"id":343,"html":344,"text":344,"type":144,"level":43},"b54","Does useFetch have the same auto-key problem as useAsyncData?",{"id":346,"html":347,"type":136},"b55","\u003Cp>Less often, because \u003Ccode>useFetch\u003C\u002Fcode>&#39;s default key already incorporates the request&#39;s method, URL, and reactive options — so a URL that varies (like \u003Ccode>\u002Fapi\u002Fproducts\u002F${id}\u003C\u002Fcode>, written reactively) naturally produces different keys. Wrapping \u003Ccode>useFetch\u003C\u002Fcode> in a composable is still worth double-checking, but the failure mode is narrower.\u003C\u002Fp>",{"id":349,"html":350,"text":350,"type":144,"level":43},"b56","Is dedupe the same thing as debouncing a request?",{"id":352,"html":353,"type":136},"b57","\u003Cp>No. Debouncing controls \u003Cem>when\u003C\u002Fem> a call is allowed to start. \u003Ccode>dedupe\u003C\u002Fcode> controls what happens when a call for a key that&#39;s \u003Cem>already in flight\u003C\u002Fem> comes in — cancel the old one and start fresh (\u003Ccode>&#39;cancel&#39;\u003C\u002Fcode>), or reuse the one already running (\u003Ccode>&#39;defer&#39;\u003C\u002Fcode>).\u003C\u002Fp>",{"id":355,"html":356,"text":356,"type":144,"level":43},"b58","How do I force a refetch from a completely different component?",{"id":358,"html":359,"type":136},"b59","\u003Cp>Call \u003Ccode>refreshNuxtData(&#39;your-key&#39;)\u003C\u002Fcode> (or \u003Ccode>clearNuxtData(&#39;your-key&#39;)\u003C\u002Fcode> to also reset the state) from wherever the triggering action lives — you don&#39;t need a reference to the original \u003Ccode>useAsyncData\u003C\u002Fcode> call, only its key.\u003C\u002Fp>",{"id":361,"html":362,"text":362,"type":144,"level":43},"b60","Do I need to worry about this if I only ever call useAsyncData once per page?",{"id":364,"html":365,"type":136},"b61","\u003Cp>Not for this specific bug — key collisions only happen when the \u003Cem>same call site\u003C\u002Fem> runs more than once with different intent. But it&#39;s worth building the reflex anyway, because &quot;once per page&quot; has a way of becoming &quot;also once inside a composable&quot; as an app grows.\u003C\u002Fp>",{"id":367,"html":368,"text":368,"type":144,"level":31},"b62","Cheat sheet",{"id":370,"head":371,"rows":375,"type":412},"b63",[372,373,374],"Task","Code","Notes",[376,380,384,388,392,396,400,404,408],[377,378,379],"Explicit key","\u003Ccode>useAsyncData(&#39;product-&#39; + id, fn)\u003C\u002Fcode>","Always do this inside any shared composable.",[381,382,383],"Implicit key (useFetch)","\u003Ccode>useFetch(() =&gt; \\\u003C\u002Fcode>\u002Fapi\u002Fproducts\u002F${id}`)`","Key derives from method + URL + options; keep the URL reactive.",[385,386,387],"Refetch on latest input only","\u003Ccode>{ watch: [query], dedupe: &#39;cancel&#39; }\u003C\u002Fcode>","Default \u003Ccode>dedupe\u003C\u002Fcode>; cancels the stale in-flight request.",[389,390,391],"Share one in-flight request","\u003Ccode>{ dedupe: &#39;defer&#39; }\u003C\u002Fcode>","For expensive\u002Fside-effect-free calls two callers might trigger at once.",[393,394,395],"Manual refetch, same component","\u003Ccode>const { refresh } = useAsyncData(...); refresh()\u003C\u002Fcode>","Also aliased \u003Ccode>execute()\u003C\u002Fcode>.",[397,398,399],"Reset without refetching","\u003Ccode>clear()\u003C\u002Fcode>","Sets \u003Ccode>data\u003C\u002Fcode>\u002F\u003Ccode>error\u003C\u002Fcode> back to empty, \u003Ccode>status\u003C\u002Fcode> to \u003Ccode>idle\u003C\u002Fcode>.",[401,402,403],"Refetch from elsewhere, by key","\u003Ccode>refreshNuxtData(&#39;product-101&#39;)\u003C\u002Fcode>","No reference to the original call needed.",[405,406,407],"Clear from elsewhere, by key","\u003Ccode>clearNuxtData(&#39;product-101&#39;)\u003C\u002Fcode>","Same idea, resets instead of refetching.",[409,410,411],"Auto-key source","file + line of the \u003Ccode>useAsyncData()\u003C\u002Fcode> call","Breaks inside wrappers called from multiple places — pass an explicit key there.","table",{"id":414,"html":415,"text":415,"type":144,"level":31},"b64","Key takeaways",{"id":417,"type":150,"items":418,"ordered":18},"b65",[419,420,421,422,423],"A \u003Ccode>useAsyncData\u003C\u002Fcode>\u002F\u003Ccode>useFetch\u003C\u002Fcode> key is the fetch&#39;s \u003Cem>identity\u003C\u002Fem>: same key means same cached entry, same in-flight request, same everything.","Nuxt auto-generates a key from the call&#39;s file and line number when you don&#39;t supply one — which silently collapses to one shared key when the call lives inside a wrapper composable called from multiple places.","\u003Ccode>dedupe: &#39;cancel&#39;\u003C\u002Fcode> (default) and \u003Ccode>dedupe: &#39;defer&#39;\u003C\u002Fcode> solve different problems — freshness versus avoiding redundant work — and picking the wrong one either serves stale data or wastes requests.","The key also decides what the server&#39;s payload hands the client to hydrate from, so a key bug is a server-side bug, not just a rendering glitch.","\u003Ccode>refresh\u003C\u002Fcode>\u002F\u003Ccode>execute\u003C\u002Fcode>, \u003Ccode>clear\u003C\u002Fcode>, \u003Ccode>refreshNuxtData\u003C\u002Fcode>, and \u003Ccode>clearNuxtData\u003C\u002Fcode> give you precise control over when a fetch actually happens — reach for them before a manual reload.",{"id":425,"html":426,"type":136},"b66","\u003Cp>Next time two pieces of UI that should be independent start mirroring each other&#39;s data, the network tab won&#39;t show you why — the fix is almost always one string, built from the right variables, passed as the second-to-last thing you&#39;d suspect: the key.\u003C\u002Fp>",{"id":428,"html":429,"type":136},"b67","\u003C!-- playground:start -->",{"id":431,"html":432,"text":432,"type":144,"level":31},"b68","🎮 Try it yourself",{"id":434,"html":435,"type":136},"b69","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnuxt-weekly-useasyncdata-keys-dedupe\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":437,"html":438,"type":136},"b70","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":440,"html":441,"type":136},"b71","\u003C!-- playground:end -->",{"id":443,"html":444,"type":136},"b72","\u003C!-- quiz:start -->",{"id":446,"html":447,"text":447,"type":144,"level":31},"b73","🧠 Test yourself",{"id":449,"html":450,"type":136},"b74","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnuxt-weekly-useasyncdata-keys-dedupe\u002Fquiz\">Take the 8-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":452,"html":453,"type":136},"b75","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":455,"html":456,"type":136},"b76","\u003C!-- quiz:end -->",{"id":458,"html":459,"type":136},"b77","\u003Cp>What&#39;s the last place a wrapper composable in your own codebase might be hiding a shared key right now? Worth a five-minute grep before it becomes a bug report.\u003C\u002Fp>",{"id":461,"type":462},"b78","divider",{"id":464,"html":465,"type":136},"b79","\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":467,"html":468,"type":136},"b80","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":470,"type":150,"items":471,"ordered":18},"b81",[472,473,474],"⭐ \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>","Two product cards on the same page, fetched with two different IDs, and both show product #1's data. No error in the console, no failed request in the network tab — `useAsyncData` did exactly what you asked it to do. The bug is that you didn't ask it what you thought you did.\n\nThis is one of the most common \"it works everywhere except in this one wrapper\" bugs in Nuxt, and it comes from a single, easy-to-miss fact: `useAsyncData`'s cache key is not derived from the data you're fetching — it's derived from *where in your source code you call it*.\n\n## What you'll learn\n\nBy the end of this article you'll be able to:\n\n- Explain how `useAsyncData` and `useFetch` generate a cache key when you don't supply one, and why that breaks inside a wrapper composable\n- Read and choose between `dedupe: 'cancel'` and `dedupe: 'defer'` for a given piece of UI\n- Trace how a key connects a server-rendered fetch to the payload the client hydrates from\n- Use `refresh()`, `clearNuxtData()`, and the `watch` option to control exactly when a re-fetch happens\n- Give every dynamic fetch an explicit, correct key, on reflex\n\n## Who this is for\n\nYou've used `useAsyncData` or `useFetch` in a Nuxt page or component at least once. You don't need to have hit this bug yet — the article builds the mental model from the first principles of what a \"key\" is for.\n\nThis article is written against **Nuxt 4.5.x** (verified against the `nuxt` package's npm dist-tags in September 2026; the latest patch at the time of writing is **4.5.2**, with Nuxt 3 having reached end-of-life on July 31, 2026). Code and directory paths use the Nuxt 4 `app\u002F` convention (`app\u002Fcomposables\u002F`, `app\u002Fpages\u002F`); if your project still uses the flat Nuxt 3 layout (`composables\u002F`, `pages\u002F` at the root), everything below works unchanged — only the folder location differs.\n\n## Table of contents\n\n- [The problem: two products, one row of data](#the-problem-two-products-one-row-of-data)\n- [The mental model: a key is the fetch's identity, not a label](#the-mental-model-a-keys-identity-not-a-label)\n- [Stage 1: the minimal useAsyncData \u002F useFetch call](#stage-1-the-minimal-useasyncdata-usefetch-call)\n- [Stage 2: what happens when you don't pass a key](#stage-2-what-happens-when-you-dont-pass-a-key)\n- [Stage 3: dedupe — cancel vs defer](#stage-3-dedupe-cancel-vs-defer)\n- [Stage 4: how the key connects server and client](#stage-4-how-the-key-connects-server-and-client)\n- [Stage 5: refreshing, clearing, and watch](#stage-5-refreshing-clearing-and-watch)\n- [Edge cases and gotchas](#edge-cases-and-gotchas)\n- [Best practices: keys and dedupe you can trust](#best-practices-keys-and-dedupe-you-can-trust)\n- [FAQ](#faq)\n- [Cheat sheet](#cheat-sheet)\n- [Key takeaways](#key-takeaways)\n\n## The problem: two products, one row of data\n\nSay you build a small wrapper composable to keep your components clean:\n\n```ts\n\u002F\u002F app\u002Fcomposables\u002FuseProduct.ts\nexport function useProduct(id: Ref\u003Cstring> | string) {\n  return useAsyncData(() => $fetch(`\u002Fapi\u002Fproducts\u002F${unref(id)}`))\n  \u002F\u002F                    ^ no key passed — looks harmless\n}\n```\n\nAnd you use it twice on the same page:\n\n```vue\n\u003Cscript setup lang=\"ts\">\nconst { data: featured } = useProduct('sku-101')\nconst { data: related } = useProduct('sku-204')\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003CProductCard :product=\"featured\" \u002F>\n  \u003CProductCard :product=\"related\" \u002F>\n  \u003C!-- both cards render sku-101 -->\n\u003C\u002Ftemplate>\n```\n\nBoth `\u003CProductCard>`s show the same product. There's no thrown error, because nothing went wrong at the network level — both requests may even have fired correctly. The bug is upstream of the network: Nuxt handed both calls the *same* cache entry, so the second call's result overwrote (or was skipped in favor of) the first's.\n\nThe instinct here is to blame `$fetch`, the API, or a race condition. It's none of those. It's the key.\n\n## The mental model: a key is the fetch's identity, not a label\n\n**The mental model:** every `useAsyncData` (and `useFetch`, which is `useAsyncData` with the URL folded in) call is backed by an entry in a shared, page-level store keyed by a string. Two calls with the *same key* are, as far as Nuxt is concerned, *the same fetch* — they share one `data` ref, one `pending` ref, one `error` ref, and one in-flight request. Two calls with *different keys* are entirely unrelated, even if they happen to call the exact same URL.\n\nThe key is not a label you attach to data you already have. It's how Nuxt decides whether two calls in your app are *asking for the same thing*. If you don't supply one, Nuxt has to invent one — and it does that with a build-time compiler step that reads the **file and line number** where `useAsyncData()` is written in your source, and turns that location into a deterministic auto-key.\n\nThat's the trap: the compiler sees the call site inside `useProduct.ts`, not inside your page. `useProduct('sku-101')` and `useProduct('sku-204')` both resolve to a call to `useAsyncData()` on the *same line of the same file* — the body of the wrapper. So both get the same auto-generated key, regardless of which ID was passed in. The wrapper hid the one piece of information Nuxt needed to tell the two calls apart.\n\n## Stage 1: the minimal useAsyncData \u002F useFetch call\n\nWritten directly in a page or component (no wrapper), an explicit key removes all ambiguity:\n\n```vue\n\u003Cscript setup lang=\"ts\">\nconst route = useRoute()\n\n\u002F\u002F Key concept: the key says \"this is product-\u003Cid>\", nothing else does.\nconst { data: product, status } = useAsyncData(\n  `product-${route.params.id}`,\n  () => $fetch(`\u002Fapi\u002Fproducts\u002F${route.params.id}`)\n)\n\u003C\u002Fscript>\n```\n\n`useFetch` is the shorthand for the common case — a call that's really just \"GET this URL, reactively\":\n\n```vue\n\u003Cscript setup lang=\"ts\">\nconst route = useRoute()\n\n\u002F\u002F useFetch derives its own key from the method, URL, and reactive options —\n\u002F\u002F you rarely need to pass one explicitly here, because the URL already\n\u002F\u002F carries the id.\nconst { data: product } = useFetch(() => `\u002Fapi\u002Fproducts\u002F${route.params.id}`)\n\u003C\u002Fscript>\n```\n\n**Key concept:** `useFetch`'s implicit key already bakes in the URL, so a *reactive* URL (an arrow function, as above) naturally produces a different key per product. That single detail is why `useFetch` feels safer than `useAsyncData` by default — its auto-key is derived from something that actually varies with your data, not from source position.\n\n## Stage 2: what happens when you don't pass a key\n\nCall `useAsyncData` directly in a page, with no wrapper in between, and the file-and-line auto-key works fine — every distinct call site in your codebase is, by definition, a distinct line. The trap only appears once you factor a `useAsyncData` call into a **shared function** that different callers invoke with different arguments — exactly the \"let's DRY this up into a composable\" instinct that Vue and Nuxt otherwise reward.\n\n```ts\n\u002F\u002F app\u002Fcomposables\u002FuseProduct.ts — the version that actually works\nexport function useProduct(id: Ref\u003Cstring> | string) {\n  return useAsyncData(\n    `product-${unref(id)}`,           \u002F\u002F key now varies with the argument\n    () => $fetch(`\u002Fapi\u002Fproducts\u002F${unref(id)}`)\n  )\n}\n```\n\nThe fix is one line: pass a key that's built from the argument, not the call site. The same rule applies to any composable you write around `useAsyncData`, `useLazyAsyncData`, or a hand-rolled data-fetching hook — **the moment a `useAsyncData` call is wrapped in a function that more than one place will call with different inputs, that key must be explicit and must include those inputs.**\n\n## Stage 3: dedupe — cancel vs defer\n\nKeys decide *whether* two calls are the same fetch. `dedupe` decides what happens when the *same key* is asked for again while a request for it is still in flight. There are exactly two values, and Nuxt defaults to `'cancel'`:\n\n- **`dedupe: 'cancel'`** (the default) — abort the in-flight request for this key and start a fresh one. Correct when only the *latest* request matters: a search box that refetches on every keystroke, a filter panel, anything where an older in-flight response would be stale by the time it arrives.\n- **`dedupe: 'defer'`** — if a request for this key is already in flight, don't start a second one; the new call reuses the pending request instead. Correct when the request is expensive or has side effects and firing it twice for the same key is simply wasteful (two components in the same render both asking for the same key at the same time, a button a user might double-click).\n\n```ts\n\u002F\u002F A typeahead: only the newest keystroke's result should win.\nconst { data: results } = useAsyncData(\n  () => `search-${query.value}`,\n  () => $fetch('\u002Fapi\u002Fsearch', { query: { q: query.value } }),\n  { watch: [query], dedupe: 'cancel' }\n)\n```\n\n```ts\n\u002F\u002F An expensive report two dashboard widgets both need at once:\n\u002F\u002F one request in flight is enough for both.\nconst { data: report } = useAsyncData(\n  'quarterly-report',\n  () => $fetch('\u002Fapi\u002Freports\u002Fquarterly'),\n  { dedupe: 'defer' }\n)\n```\n\n**Key concept:** `dedupe` is not debouncing. Debouncing delays *starting* a call; `dedupe` decides what happens to calls that have *already* started for the same key. You often want both — debounce the keystroke, then let `dedupe: 'cancel'` handle any request that still overlaps.\n\n## Stage 4: how the key connects server and client\n\nDuring SSR, Nuxt resolves your page's `useAsyncData`\u002F`useFetch` calls and serializes the results into a **payload** — a plain-data snapshot sent to the browser alongside the HTML, keyed by exactly the same strings you've been reading about. On the client, hydration doesn't re-run your fetches from scratch: for each key, Nuxt's `getCachedData` step looks in that payload first. If the key is present, the cached value is used immediately and the handler function never runs on the client at all.\n\nThis is the other reason a duplicated auto-key is dangerous, not just cosmetically wrong: it means the *server* also only ran one fetch for what you thought were two different pieces of data, so the payload only ever contained product #1's response in the first place. The bug isn't purely a client-side rendering glitch — the wrong data was fetched once, on the server, and faithfully shipped to the browser.\n\n*(If server-side state feels shaky in general, the previous episode in this series, [Nuxt useState vs ref(): Why Server State Leaks Across Users](https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fnuxt-usestate-vs-ref-why-server-state-leaks-across-users-47n1), covers the sibling bug — module-scope state shared across *requests* rather than across calls on the same page. Useful background, not required reading.)*\n\n## Stage 5: refreshing, clearing, and watch\n\n`useAsyncData` returns more than `data`: `status`, `pending`, `error`, `refresh` (an alias, `execute`, does the same thing), and `clear`.\n\n```vue\n\u003Cscript setup lang=\"ts\">\nconst { data: product, refresh, clear } = useAsyncData(\n  `product-${route.params.id}`,\n  () => $fetch(`\u002Fapi\u002Fproducts\u002F${route.params.id}`)\n)\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cbutton @click=\"refresh()\">Reload this product\u003C\u002Fbutton>\n  \u003Cbutton @click=\"clear()\">Reset\u003C\u002Fbutton>\n\u003C\u002Ftemplate>\n```\n\n- **`refresh()` \u002F `execute()`** — re-runs the handler for this call's key and updates `data` in place. This is the correct way to force a re-fetch from *inside* the component that owns the call.\n- **`clear()`** — resets `data` to `undefined` (or the configured default), `error` to `undefined`, and `status` to `idle`, without refetching.\n- **`refreshNuxtData(key?)` \u002F `clearNuxtData(key?)`** — the same two operations, callable from anywhere, by key, when you don't have a reference to the original composable call (a \"save\" action in one component that should invalidate a list rendered in another).\n- **`watch: [...]`** — an array of reactive sources; when any of them changes, Nuxt automatically calls `refresh()` for you. This is what makes Stage 3's search example refetch on every keystroke without a manual `watch()` block of your own.\n\n**Key concept:** changing the *key* and adding a *watch source* are two different ways to make a fetch reactive, and they're not interchangeable. A changing key gives you a distinct cache entry per value (useful when you want to keep old results around, like cached pages of a paginated list). A `watch` source refetches the *same* entry in place, overwriting it (useful when you only ever care about the current value, like a live search).\n\n## Edge cases and gotchas\n\n- **Route params without a param-derived key.** `useAsyncData('product', ...)` on a dynamic `[id].vue` page reuses one cache entry across every product route the user navigates to client-side — the classic \"stale data flashes for a moment on navigation\" bug. Always fold the param into the key.\n- **`useLazyAsyncData` doesn't block navigation, but it's the same key machinery.** A lazy call that shares a key with a non-lazy call elsewhere on the page still participates in the same dedupe and cache entry — \"lazy\" only changes whether Nuxt awaits it before rendering, not how its key behaves.\n- **Two components requesting the same key on the same page is often intentional, not a bug** — it's exactly how Nuxt avoids two network round-trips for data two widgets both need. The failure mode in this article is the opposite: keys colliding when you *didn't* want them to.\n- **`dedupe: 'defer'` on a request with side effects can surprise you.** If the \"duplicate\" call actually needed to trigger a fresh side effect (an analytics ping baked into the handler, say), deferring silently skips it. Keep handlers free of side effects that must run every time they're called.\n- **SSR + hydration mismatch from a key that differs between server and client.** If your key computation depends on something only available client-side (e.g., `window`-derived state), the server and client will disagree on the key, and the client will refetch instead of hydrating from the payload — usually harmless, but worth knowing so it doesn't look like a hydration bug elsewhere.\n\n## Best practices: keys and dedupe you can trust\n\n- **Reach for an explicit key** the moment a `useAsyncData` call sits inside a composable, a loop, or anywhere more than one logical \"thing\" could call it. The rule of thumb: if the data being fetched varies, the key must vary with it.\n- **Build the key from the same values the handler uses** — a template literal with every dynamic input (`\\`product-${id}\\``, not just `'product'`) is the whole fix, every time.\n- **Prefer `useFetch`** for the plain \"reactively GET this URL\" case — its implicit key already includes the URL, so you get the safety of an explicit key for free.\n- **Choose `dedupe` by cost and freshness, not by habit.** Default (`'cancel'`) for anything driven by fast user input where only the latest answer matters; `'defer'` for expensive or side-effect-bearing calls that multiple simultaneous callers can safely share.\n- **Invalidate by key, not by reload.** Reach for `refresh()`\u002F`refreshNuxtData()` and `clear()`\u002F`clearNuxtData()` before reaching for a full page reload or a manually toggled `key` prop to force a remount.\n\n## FAQ\n\n### Why did my two components end up showing the same data with no error?\n\nBecause both `useAsyncData` calls resolved to the same auto-generated key — most often because both went through the same line of a shared wrapper composable. Nuxt didn't fail; it correctly treated two calls with the same key as one fetch.\n\n### Does useFetch have the same auto-key problem as useAsyncData?\n\nLess often, because `useFetch`'s default key already incorporates the request's method, URL, and reactive options — so a URL that varies (like `\u002Fapi\u002Fproducts\u002F${id}`, written reactively) naturally produces different keys. Wrapping `useFetch` in a composable is still worth double-checking, but the failure mode is narrower.\n\n### Is dedupe the same thing as debouncing a request?\n\nNo. Debouncing controls *when* a call is allowed to start. `dedupe` controls what happens when a call for a key that's *already in flight* comes in — cancel the old one and start fresh (`'cancel'`), or reuse the one already running (`'defer'`).\n\n### How do I force a refetch from a completely different component?\n\nCall `refreshNuxtData('your-key')` (or `clearNuxtData('your-key')` to also reset the state) from wherever the triggering action lives — you don't need a reference to the original `useAsyncData` call, only its key.\n\n### Do I need to worry about this if I only ever call useAsyncData once per page?\n\nNot for this specific bug — key collisions only happen when the *same call site* runs more than once with different intent. But it's worth building the reflex anyway, because \"once per page\" has a way of becoming \"also once inside a composable\" as an app grows.\n\n## Cheat sheet\n\n| Task | Code | Notes |\n| --- | --- | --- |\n| Explicit key | `useAsyncData('product-' + id, fn)` | Always do this inside any shared composable. |\n| Implicit key (useFetch) | `useFetch(() => \\`\u002Fapi\u002Fproducts\u002F${id}\\`)` | Key derives from method + URL + options; keep the URL reactive. |\n| Refetch on latest input only | `{ watch: [query], dedupe: 'cancel' }` | Default `dedupe`; cancels the stale in-flight request. |\n| Share one in-flight request | `{ dedupe: 'defer' }` | For expensive\u002Fside-effect-free calls two callers might trigger at once. |\n| Manual refetch, same component | `const { refresh } = useAsyncData(...); refresh()` | Also aliased `execute()`. |\n| Reset without refetching | `clear()` | Sets `data`\u002F`error` back to empty, `status` to `idle`. |\n| Refetch from elsewhere, by key | `refreshNuxtData('product-101')` | No reference to the original call needed. |\n| Clear from elsewhere, by key | `clearNuxtData('product-101')` | Same idea, resets instead of refetching. |\n| Auto-key source | file + line of the `useAsyncData()` call | Breaks inside wrappers called from multiple places — pass an explicit key there. |\n\n## Key takeaways\n\n- A `useAsyncData`\u002F`useFetch` key is the fetch's *identity*: same key means same cached entry, same in-flight request, same everything.\n- Nuxt auto-generates a key from the call's file and line number when you don't supply one — which silently collapses to one shared key when the call lives inside a wrapper composable called from multiple places.\n- `dedupe: 'cancel'` (default) and `dedupe: 'defer'` solve different problems — freshness versus avoiding redundant work — and picking the wrong one either serves stale data or wastes requests.\n- The key also decides what the server's payload hands the client to hydrate from, so a key bug is a server-side bug, not just a rendering glitch.\n- `refresh`\u002F`execute`, `clear`, `refreshNuxtData`, and `clearNuxtData` give you precise control over when a fetch actually happens — reach for them before a manual reload.\n\nNext time two pieces of UI that should be independent start mirroring each other's data, the network tab won't show you why — the fix is almost always one string, built from the right variables, passed as the second-to-last thing you'd suspect: the key.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnuxt-weekly-useasyncdata-keys-dedupe\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 8-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnuxt-weekly-useasyncdata-keys-dedupe\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\nWhat's the last place a wrapper composable in your own codebase might be hiding a shared key right now? Worth a five-minute grep before it becomes a bug report.\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":107,"canonical":477,"description":108},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnuxt-weekly-useasyncdata-keys-dedupe","01a08c59-464c-7039-acf3-b56677bffbbe",{"name":480,"part":31,"total":31,"items":481},"Nuxt Deep Dive",[482,486],{"slug":483,"title":484,"publishedAt":485,"readingMinutes":83},"nuxt-weekly-cross-request-state-leak","Nuxt useState vs ref(): Why Server State Leaks Across Users","2026-08-30T11:37:11.335Z",{"slug":46,"title":107,"publishedAt":110,"readingMinutes":83},{"id":488,"locked":18},"01a08c59-469c-7678-aeb1-510ef04ab298",[490],{"id":45,"slug":46,"title":48,"_count":491},{"questions":51},[493],{"locale":13,"slug":46},{"id":45,"slug":46,"title":48,"_count":495,"questionCount":51},{"questions":51}]