[{"data":1,"prerenderedAt":4},["ShallowReactive",2],{"post-content-nuxt-usefetch-vs-fetch-double-fetch-trap":3},"\u003Cp data-page-node-id=\"HHr7YWCCwm4Ga1DD8Jmn6U\">Last month I was reviewing a client's Nuxt app before launch, network tab open, watching requests scroll past. A page loaded, and there it was: \u003Ccode data-page-node-id=\"a3XsvVuYVaruqa8GE7ud5u\">\u002Fapi\u002Fposts\u003C\u002Fcode> firing twice. Same endpoint, same params, once from the server render and once more the moment the browser hydrated. The app worked fine. Nobody noticed anything. But every page load was quietly paying for the same request twice.\u003C\u002Fp>\n\n\u003Cp data-page-node-id=\"t3c3eSLIC3EAihEzuF9TMr\">If you've written any Nuxt code, you've probably done what I did on my first project: reached for \u003Ccode data-page-node-id=\"glWWPvacXoTqbgWdsJ39yC\">$fetch\u003C\u002Fcode> everywhere because it looks like the normal way to fetch things. It took me an embarrassing number of pages before someone pointed out the actual difference between \u003Ccode data-page-node-id=\"eXjV2ddFArPWFS0Q3rA2fn\">$fetch\u003C\u002Fcode>, \u003Ccode data-page-node-id=\"QTKLTOJV3gDQe1PdlPu3ed\">useFetch\u003C\u002Fcode>, and \u003Ccode data-page-node-id=\"Jmxw8BiaGTRKlW8J3SU4hJ\">useAsyncData\u003C\u002Fcode>, and why the choice matters for every page that renders on the server.\u003C\u002Fp>\n\n\u003Cimg src=\"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1498050108023-c5249f4df085?w=1200&q=80&auto=format&fit=crop\" alt=\"Laptop showing JavaScript code during development\" loading=\"lazy\" data-page-node-id=\"n7g94u2nCMehAZuCE6VTqg\" \u002F>\n\n\u003Ch2 data-page-node-id=\"OWTu9DH2qrxc6VRvAYCLwo\">The one fact that explains everything\u003C\u002Fh2>\n\n\u003Cp data-page-node-id=\"iiE95HcdsehWd7KUrAi28c\">\u003Ccode data-page-node-id=\"iQIB7spoScsywRhxTzoXfP\">$fetch\u003C\u002Fcode> is just ofetch with a global alias. It knows nothing about SSR. When you call it in a component's setup block on a server-rendered page, it runs once on the server to produce HTML, and then runs again in the browser during hydration, because nothing carries the server's result over. The client has no idea the data already exists.\u003C\u002Fp>\n\n\u003Cp data-page-node-id=\"fUw0yNiqSzEcTTDLzUoDWV\">\u003Ccode data-page-node-id=\"yWtfySSNMQkN9noaxrLnJh\">useFetch\u003C\u002Fcode> and \u003Ccode data-page-node-id=\"JEd2YjEThMzGb2W3V1nzrk\">useAsyncData\u003C\u002Fcode> fix exactly this. They serialize the server-side result into the Nuxt payload, and during hydration the client reads it from there instead of re-requesting. One request, total.\u003C\u002Fp>\n\n\u003Cpre data-page-node-id=\"zcMdW1KwHZPW1nNIoMdXVZ\">\u003Ccode class=\"language-vue\" data-page-node-id=\"CU0YAMu5NDC57qhW6nNrSA\">\u002F\u002F Anti-pattern: fires on the server AND again on the client\nconst posts = await $fetch('\u002Fapi\u002Fposts')\n\n\u002F\u002F Fetches once on the server, hydrates from the payload\nconst { data: posts } = await useFetch('\u002Fapi\u002Fposts')\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp data-page-node-id=\"ikvk2zraz9HrMlUYGiKEgo\">That's the whole mental model. Everything else is picking which tool fits which call.\u003C\u002Fp>\n\n\u003Ch2 data-page-node-id=\"DhcefMI6fYYdApgPV7EuAV\">So when is $fetch the right call?\u003C\u002Fh2>\n\n\u003Cp data-page-node-id=\"ALQ6CfFHLv9J4UWaEz3Aq3\">Whenever the request comes from a user action in the browser. Form submits, button clicks, search-as-you-type. No SSR involved, no payload to worry about:\u003C\u002Fp>\n\n\u003Cpre data-page-node-id=\"npUrLoi94jt0DdYB9U4Xmk\">\u003Ccode class=\"language-vue\" data-page-node-id=\"L3cDN1CtGGkHsKQcnfntLh\">async function submit() {\n  await $fetch('\u002Fapi\u002Fcontact', {\n    method: 'POST',\n    body: formData.value,\n  })\n  await refresh() \u002F\u002F re-run the useFetch above if you list data\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp data-page-node-id=\"ioGuW4J1AbuSF1MJbVXS4i\">There's a second place \u003Ccode data-page-node-id=\"eFq2QMa9ATMPwpCMUBKW60\">$fetch\u003C\u002Fcode> shines that surprised me: calling your own server routes from server code. When a server route calls \u003Ccode data-page-node-id=\"gNJD3xlc39F31XGO59iFTN\">$fetch('\u002Fapi\u002Fuser')\u003C\u002Fcode>, Nuxt skips the network entirely and calls the handler function in the same process. No real HTTP round trip. Calling external APIs still goes over the wire, of course.\u003C\u002Fp>\n\n\u003Ch2 data-page-node-id=\"7l57Z81FCweTl75qGNLdxA\">useFetch or useAsyncData?\u003C\u002Fh2>\n\n\u003Cp data-page-node-id=\"T71rFvofEIUW5MUPo3nWFf\">They return the same object and share the same options. \u003Ccode data-page-node-id=\"bsheTGBbHxLlw1ZTzZIV8L\">useFetch(url)\u003C\u002Fcode> is basically \u003Ccode data-page-node-id=\"aLkXBnosoE4heFIwdydaF8\">useAsyncData(key, () =&gt; $fetch(url))\u003C\u002Fcode>. So the rule is simple: one URL, use \u003Ccode data-page-node-id=\"joxdTayIWwT8gjNBk9OLHF\">useFetch\u003C\u002Fcode>. Anything more complicated, reach for \u003Ccode data-page-node-id=\"QHIZRaDoNUCHv872iyMlUG\">useAsyncData\u003C\u002Fcode>, which wraps any async function:\u003C\u002Fp>\n\n\u003Cpre data-page-node-id=\"TBvi80Msgt2VUZbs4jGteC\">\u003Ccode class=\"language-vue\" data-page-node-id=\"Y3Lv8n5FhgPNAoWr7NIeRV\">const { data, error } = await useAsyncData('dashboard', async () =&gt; {\n  const [user, stats, notifications] = await Promise.all([\n    $fetch('\u002Fapi\u002Fuser'),\n    $fetch('\u002Fapi\u002Fstats'),\n    $fetch('\u002Fapi\u002Fnotifications'),\n  ])\n  return { user, stats, notifications }\n})\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp data-page-node-id=\"Zc3Vf5c0OPPoIrEIgN5Acs\">\u003Ccode data-page-node-id=\"kM0qsAr9pCbDhEFARdCRVv\">useFetch\u003C\u002Fcode> can't do this because it only handles a single endpoint. Also worth knowing: if your page data comes from a database or another non-HTTP source, \u003Ccode data-page-node-id=\"op1PBLQFU1e6Amu83IbBgW\">useAsyncData\u003C\u002Fcode> is the only option of the two.\u003C\u002Fp>\n\n\u003Ch2 data-page-node-id=\"GV1pkHNOpv6xvmH6sBkoYg\">Four gotchas that actually bit me\u003C\u002Fh2>\n\n\u003Cp data-page-node-id=\"8fY6YhRm9Z9lyE6UzxnebY\">The return shape gives you \u003Ccode data-page-node-id=\"jPYeKm3TZQPsw3YEOctCiH\">data\u003C\u002Fcode>, \u003Ccode data-page-node-id=\"kMfk50D3rkYYfw8raJ6kza\">status\u003C\u002Fcode> (\u003Ccode data-page-node-id=\"QitEptkpkrvH6v5HneLD98\">'idle' | 'pending' | 'success' | 'error'\u003C\u002Fcode>), \u003Ccode data-page-node-id=\"gYaRyRDEmCdEtX5yhrPQqa\">error\u003C\u002Fcode>, and \u003Ccode data-page-node-id=\"j9ovQRw2buH1ehBWB3oknh\">refresh\u003C\u002Fcode>. Prefer checking \u003Ccode data-page-node-id=\"9PSAIKLNCQJ273Gml91eiH\">status\u003C\u002Fcode> over the \u003Ccode data-page-node-id=\"70znuvANy9IZNFDZwy942r\">pending\u003C\u002Fcode> ref. Beyond that, a few options have teeth:\u003C\u002Fp>\n\n\u003Cul data-page-node-id=\"jllfrntb5KivXFbZPASAWU\">\n\u003Cli data-page-node-id=\"qHJLBObpHdBf8JkyiiLh7h\">\u003Cstrong data-page-node-id=\"SN0iXbFEC7Ohflgfmk7o92\">Reactive params.\u003C\u002Fstrong> Pass a ref or computed as the url, query, or body and Nuxt refetches when it changes. But a plain interpolated string like \u003Ccode data-page-node-id=\"Xovt9Cat3nPi2eYoJWCCsY\">useFetch(`\u002Fapi\u002Fposts\u002F${id.value}`)\u003C\u002Fcode> is captured once and will not react. Use the options object for dynamic values.\u003C\u002Fli>\n\u003Cli data-page-node-id=\"c17YBAPdUoDyWmzGg8eaHo\">\u003Cstrong data-page-node-id=\"iVWZpkLDDhT6qs74beRZAm\">await and lazy are independent.\u003C\u002Fstrong> Without \u003Ccode data-page-node-id=\"7ld9vA6jhmvDEk5FZxMjio\">await\u003C\u002Fcode>, code after the call runs immediately and client-side navigation doesn't block. That sounds like \u003Ccode data-page-node-id=\"fAfEV0plUnBDhJXsXeh5us\">lazy: true\u003C\u002Fcode>, but they're not identical: if you \u003Ccode data-page-node-id=\"QAYg4Bgpz65fJndKL9Ktdt\">await\u003C\u002Fcode> a lazy call, the await resolves instantly on client-side navigation anyway. If you want navigation to wait for data, drop \u003Ccode data-page-node-id=\"DE5iKRlwtqnEyDTVEvYw1R\">lazy\u003C\u002Fcode>. If you want it non-blocking, use \u003Ccode data-page-node-id=\"GJ8xzFW8ckO4SqGPpTUC64\">lazy\u003C\u002Fcode> explicitly instead of just skipping the await.\u003C\u002Fli>\n\u003Cli data-page-node-id=\"ew7Ea8EY0uBrb3BLE1qPX7\">\u003Cstrong data-page-node-id=\"hxf7I2EFxXa3Eulh7UptGJ\">data is a shallowRef by default.\u003C\u002Fstrong> Mutating a nested property won't trigger updates. Either replace the whole value or pass \u003Ccode data-page-node-id=\"muJ2TUK3l5aPLJkJueFjn5\">deep: true\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli data-page-node-id=\"PfZxVOjafY3ojo2YkbUbrg\">\u003Cstrong data-page-node-id=\"KZBCfCe1cXLs36ocdcZ6O3\">dedupe defaults to 'cancel'.\u003C\u002Fstrong> Two concurrent calls with the same key means the first gets aborted. Fine most of the time, confusing if you fire the same key from two components and expect both to finish.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp data-page-node-id=\"0pvoN0zMlmBvhYcaNmVAzX\">One more that costs nothing and saves debugging time: \u003Ccode data-page-node-id=\"zBl7rkUV6nDIxEZ8fbEvk2\">transform\u003C\u002Fcode> runs before the payload is serialized, and \u003Ccode data-page-node-id=\"x3hPeCTquyB80fWbAxYzKU\">pick\u003C\u002Fcode> keeps only the keys you list. Both shrink the payload that ships to the browser. If your API returns fat objects, this is free bandwidth.\u003C\u002Fp>\n\n\u003Ch2 data-page-node-id=\"7nJAHS9kA60zV63SfDx3Gh\">The five-second check\u003C\u002Fh2>\n\n\u003Cp data-page-node-id=\"GyjQI1Ic3Y8gmwW9MWRi6z\">Open Nuxt DevTools and look at the Payload tab. If your page's data is in there, hydration reused it and you did it right. If it's missing and you see the request fire again in the network tab, something in that component is using \u003Ccode data-page-node-id=\"9dllLdSR77U3BUDmyB6lp0\">$fetch\u003C\u002Fcode> where it should use \u003Ccode data-page-node-id=\"AWvM4fKQyiLkHZ3z9PxDKz\">useFetch\u003C\u002Fcode>.\u003C\u002Fp>\n\n\u003Cp data-page-node-id=\"kxjwUXvFoiAeiKPNgUgtJE\">This pairs nicely with the server routes I wrote about earlier (\u003Ca href=\"\u002Fposts\u002Freplaced-express-api-nuxt-server-routes\u002F\" data-page-node-id=\"kxJEsgE40nAtZBEMbCkP8m\">when I replaced an Express API with Nuxt server routes\u003C\u002Fa>): once your backend lives in the same app, internal \u003Ccode data-page-node-id=\"ZoQH27eKBpEvQFiB4w10Nz\">$fetch\u003C\u002Fcode> calls become direct function calls, and the only requests worth spending are the ones the payload can't cover.\u003C\u002Fp>\n\n\u003Cp data-page-node-id=\"v0b8WeIJ8n4UGvzF7j7h2N\">I keep a small cheat sheet of these composables in \u003Ca href=\"\u002Fsnippetark\u002F\" data-page-node-id=\"9eUx4XZa1VNfMapgonzdzc\">Snippet Ark\u003C\u002Fa>, because this is exactly the kind of thing I re-derive from scratch every six months and then feel silly about. Maybe you do too.\u003C\u002Fp>\n",1789138766799]