{"id":8384,"date":"2026-01-28T09:26:35","date_gmt":"2026-01-28T14:26:35","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=8384"},"modified":"2026-01-28T09:26:36","modified_gmt":"2026-01-28T14:26:36","slug":"single-flight-mutations-in-tanstack-start-part-2","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/single-flight-mutations-in-tanstack-start-part-2\/","title":{"rendered":"Single Flight Mutations in TanStack Start: Part 2"},"content":{"rendered":"\n<p><a href=\"https:\/\/frontendmasters.com\/blog\/single-flight-mutations-in-tanstack-start-part-1\/\">In part 1<\/a>, we talked about how single flight mutations allow you to update data, and re-fetch all the relevant updated data for the UI, all in just one roundtrip across the network. <\/p>\n\n\n\n<p>We implemented a trivial solution for this, which is to say that we threw caution (and coupling) to the wind and just re-fetched what we needed in the server function we had for updating data. This worked fine, but it was hardly scalable, or flexible.<\/p>\n\n\n<div class=\"box article-series\">\n  <header>\n    <h3 class=\"article-series-header\">Article Series<\/h3>\n  <\/header>\n  <div class=\"box-content\">\n            <ol>\n                      <li>\n              <a href=\"https:\/\/frontendmasters.com\/blog\/single-flight-mutations-in-tanstack-start-part-1\/\">Single Flight Mutations in TanStack Start: Part 1<\/a>\n            <\/li>\n                      <li>\n              <a href=\"https:\/\/frontendmasters.com\/blog\/single-flight-mutations-in-tanstack-start-part-2\/\">Single Flight Mutations in TanStack Start: Part 2<\/a>\n            <\/li>\n                  <\/ol>\n        <\/div>\n<\/div>\n\n\n\n<p>In this post, we&#8217;ll accomplish the same thing, but in a much more flexible way. We&#8217;ll define some refetching middleware that we can attach to any server function. The middleware will allow us to specify, via react-query keys, what data we want re-fetched, and it&#8217;ll handle everything from there.<\/p>\n\n\n\n<p>We&#8217;ll start simple, and keep on adding features and flexibility. Things will get a bit complex by the end, but please don&#8217;t think you need to use everything we&#8217;ll talk about. In fact, for the vast majority of apps, single flight mutations probably won&#8217;t matter at all. And don&#8217;t be fooled: simply re-fetching some data in a server function might be good enough for a lot of smaller apps as well.<\/p>\n\n\n\n<p>But in going through all of this we&#8217;ll get to see some really cool TanStack, and even TypeScript features. Even if you never use what we go over for single flight mutations, there&#8217;s a good chance this content will come in handy for something else.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"our-first-middleware\">Our First Middleware<\/h2>\n\n\n\n<p><a href=\"https:\/\/tanstack.com\/query\/latest\">TanStack Query<\/a> (which we sometimes refer to as react-query, it&#8217;s package name) already has a wonderful system of hierarchical keys. Wouldn&#8217;t it be great if we could just have our middleware receive the query keys of what we want to refetch, and have it just&#8230; work? Have the middleware figure out <em>how<\/em> to refetch does seem tricky, at first. Sure, our queries have all been simple calls (by design) to server functions. But we can&#8217;t pass a server function reference up to the server; functions are not serializable. How could they be? You can send strings and numbers (and booleans) across the wire, serialized as JSON, but sending a function (which can have state, close over context, etc) makes no sense.<\/p>\n\n\n\n<p><em>Unless<\/em> they&#8217;re TanStack Start server functions, that is. <\/p>\n\n\n\n<p>It turns out the incredible engineers behind this project customized their serialization engine to support server functions. That means you can send a server function to the server, from the client, and it will work fine. Under the covers, server functions have an internal ID. TanStack picks this up, sends the ID, and then de-serializes the ID on the other end.<\/p>\n\n\n\n<p>To make this even easier, why don\u2019t we just attach the server function (and the argument it takes) right in to the query options we already have defined. Then our middleware can take the query keys we want re-fetched, look up the query from TanStack Query internals (which we&#8217;ll dive into) and just make everything work.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"lets-get-started\">Let&#8217;s Get Started<\/h2>\n\n\n\n<p>First we&#8217;ll import some goodies<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">import<\/span> { createMiddleware, getRouterInstance } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@tanstack\/react-start\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { QueryClient, QueryKey } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@tanstack\/react-query\"<\/span>;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Next, let&#8217;s update our query options for our epics list query (the main list of epics).<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> epicsQueryOptions = <span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">page<\/span>: <span class=\"hljs-params\">number<\/span><\/span>) =&gt;<\/span> {\n  <span class=\"hljs-keyword\">return<\/span> queryOptions({\n    queryKey: &#91;<span class=\"hljs-string\">\"epics\"<\/span>, <span class=\"hljs-string\">\"list\"<\/span>, page],\n    queryFn: <span class=\"hljs-keyword\">async<\/span> () =&gt; {\n      <span class=\"hljs-keyword\">const<\/span> result = <span class=\"hljs-keyword\">await<\/span> getEpicsList({ data: page });\n      <span class=\"hljs-keyword\">return<\/span> result;\n    },\n    staleTime: <span class=\"hljs-number\">1000<\/span> * <span class=\"hljs-number\">60<\/span> * <span class=\"hljs-number\">5<\/span>,\n    gcTime: <span class=\"hljs-number\">1000<\/span> * <span class=\"hljs-number\">60<\/span> * <span class=\"hljs-number\">5<\/span>,\n    meta: {\n      __revalidate: {\n        serverFn: getEpicsList,\n        arg: page,\n      },\n    },\n  });\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Note the new <code>meta<\/code> section. This allows us to add any random metadata that we want to our query. Here we send over a reference to the <code>getEpicsList<\/code> server function, and the arg it takes. If this duplication makes you uneasy, stay tuned. We&#8217;ll also update the summary query (for the counts) the same way, though that&#8217;s not shown here.<\/p>\n\n\n\n<p>Let&#8217;s build this middleware piece by piece.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-comment\">\/\/ the server function and args are all `any`, for now, <\/span>\n<span class=\"hljs-comment\">\/\/ to keep things simple we'll see how to type them in a bit<\/span>\n<span class=\"hljs-keyword\">type<\/span> RevalidationPayload = {\n  refetch: {\n    key: QueryKey;\n    fn: <span class=\"hljs-built_in\">any<\/span>;\n    arg: <span class=\"hljs-built_in\">any<\/span>;\n  }&#91;];\n};\n\n<span class=\"hljs-keyword\">type<\/span> RefetchMiddlewareConfig = {\n  refetch: QueryKey&#91;];\n};\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> refetchMiddleware = createMiddleware({ <span class=\"hljs-keyword\">type<\/span>: <span class=\"hljs-string\">\"function\"<\/span> })\n  .inputValidator(<span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">config<\/span>?: <span class=\"hljs-params\">RefetchMiddlewareConfig<\/span><\/span>) =&gt;<\/span> config)\n  .client(<span class=\"hljs-keyword\">async<\/span> ({ next, data }) =&gt; {\n    <span class=\"hljs-keyword\">const<\/span> { refetch = &#91;] } = data ?? {};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We define an input to the middleware. This input will automatically get&nbsp;<em>merged<\/em>&nbsp;with whatever input is defined on any server function this middleware winds up attached to.<\/p>\n\n\n\n<p>We define our input as optional (<code>config?<\/code>) since it&#8217;s entirely possible we might want to sometimes call our server function and simply not refetch anything.<\/p>\n\n\n\n<p>Now we start our client callback, which runs directly in our browser. We&#8217;ll first grab the array of query keys we want refetched.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> { refetch = &#91;] } = data ?? {};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Then we&#8217;ll get our&nbsp;<code>queryClient<\/code>&nbsp;and the cache attached to it, and define the payload we&#8217;ll send to the server callback of our middleware, which will do the actual refetching.<\/p>\n\n\n\n<p>If you&#8217;ve never touched TanStack&#8217;s middleware before and are feeling overwhelmed, my <a href=\"https:\/\/frontendmasters.com\/blog\/introducing-tanstack-start-middleware\/\">middleware post<\/a> might be a good introduction.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> router = <span class=\"hljs-keyword\">await<\/span> getRouterInstance();\n<span class=\"hljs-keyword\">const<\/span> queryClient: QueryClient = router.options.context.queryClient;\n<span class=\"hljs-keyword\">const<\/span> cache = queryClient.getQueryCache();\n\n<span class=\"hljs-keyword\">const<\/span> revalidate: RevalidationPayload = {\n  refetch: &#91;],\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Our <code>queryClient<\/code> is already attached to the main TanStack router context, so we can get the router, and just grab it.<\/p>\n\n\n\n<p>Remember before when we added that <code>__revalidate<\/code> payload to our query options, with the server function, and arg? Let&#8217;s look in our query cache for each key, and retrieve the query options for them.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\">refetch.forEach(<span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">key<\/span>: <span class=\"hljs-params\">QueryKey<\/span><\/span>) =&gt;<\/span> {\n  <span class=\"hljs-keyword\">const<\/span> entry = cache.find({ queryKey: key, exact: <span class=\"hljs-literal\">true<\/span> });\n  <span class=\"hljs-keyword\">if<\/span> (!entry) <span class=\"hljs-keyword\">return<\/span>;\n\n  <span class=\"hljs-keyword\">const<\/span> revalidatePayload: <span class=\"hljs-built_in\">any<\/span> = entry?.meta?.__revalidate ?? <span class=\"hljs-literal\">null<\/span>;\n\n  <span class=\"hljs-keyword\">if<\/span> (revalidatePayload) {\n    revalidate.refetch.push({\n      key,\n      fn: revalidatePayload.serverFn,\n      arg: revalidatePayload.arg,\n    });\n  }\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The check <code>if (!entry) return;<\/code> protects us from refetches being requested for queries that don&#8217;t exist in cache\u2014ie, if they&#8217;ve never been fetched in the UI. If that happens, just skip to the next one. We have no way to refetch it if we don&#8217;t have the <code>serverFn<\/code>.<\/p>\n\n\n\n<p>You could expand the input to this middleware and send up a different payload of query keys, along with the actual refetching payload (including server function and arg) for queries you absolutely want run, even if they haven&#8217;t yet been requested. Perhaps you&#8217;re planning on redirecting after the mutation, and you want that new page&#8217;s data prefetched. We won&#8217;t implement that here, but it&#8217;s just a variation on this same theme. These pieces are all very composable, so build whatever you happen to need!<\/p>\n\n\n\n<p>This code then grabs the meta object, and puts the properties onto the payload we&#8217;ll send to the server.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> revalidatePayload: <span class=\"hljs-built_in\">any<\/span> = entry?.meta?.__revalidate ?? <span class=\"hljs-literal\">null<\/span>;\n\n<span class=\"hljs-keyword\">if<\/span> (revalidatePayload) {\n  revalidate.refetch.push({\n    key,\n    fn: revalidatePayload.serverFn,\n    arg: revalidatePayload.arg,\n  });\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"learn-more\">Try not to let the various <code>any<\/code> types bother you; I&#8217;m omitting some type definitions that would have been straightforward to define, in order to help prevent this long post from getting even longer.<\/p>\n\n\n\n<p>Calling <code>next<\/code> triggers the actual invocation of the server function (and any other middleware in the chain). The <code>sendContext<\/code> arg allows us to send data <em>from<\/em> the client, <em>up to<\/em> the server. The server is allowed to call <code>next<\/code> with a <code>sendContext<\/code> payload that sends data back to the client.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> result = <span class=\"hljs-keyword\">await<\/span> next({\n  sendContext: {\n    revalidate,\n  },\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The <code>result<\/code> payload is what comes back from the server function invocation. The context object on it will have a payloads array, returned from the <code>.server<\/code> callback just below, with entries containing a key (the query key), and result (the actual data). We&#8217;ll loop it, and update the query data accordingly.<\/p>\n\n\n\n<p>We&#8217;ll fix the TS error covered up with <code>\/\/ @ts-expect-error<\/code> momentarily.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-comment\">\/\/ @ts-expect-error<\/span>\n<span class=\"hljs-keyword\">for<\/span> (<span class=\"hljs-keyword\">const<\/span> entry of result.context?.payloads ?? &#91;]) {\n  queryClient.setQueryData(entry.key, entry.result);\n}\n\n<span class=\"hljs-keyword\">return<\/span> result;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h2 class=\"wp-block-heading\" id=\"the-server-callback\">The Server Callback<\/h2>\n\n\n\n<p>The server callback looks like this, in its entirety.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\">.server(<span class=\"hljs-keyword\">async<\/span> ({ next, context }) =&gt; {\n  <span class=\"hljs-keyword\">const<\/span> result = <span class=\"hljs-keyword\">await<\/span> next({\n    sendContext: {\n      payloads: &#91;] <span class=\"hljs-keyword\">as<\/span> <span class=\"hljs-built_in\">any<\/span>&#91;]\n    }\n  });\n\n  <span class=\"hljs-keyword\">const<\/span> allPayloads = context.revalidate.refetch.map(<span class=\"hljs-function\"><span class=\"hljs-params\">refetchPayload<\/span> =&gt;<\/span> {\n    <span class=\"hljs-keyword\">return<\/span> {\n      key: refetchPayload.key,\n      result: refetchPayload.fn({ data: refetchPayload.arg })\n    };\n  });\n\n  <span class=\"hljs-keyword\">for<\/span> (<span class=\"hljs-keyword\">const<\/span> refetchPayload of allPayloads) {\n    result.sendContext.payloads.push({\n      key: refetchPayload.key,\n      result: <span class=\"hljs-keyword\">await<\/span> refetchPayload.result\n    });\n  }\n\n  <span class=\"hljs-keyword\">return<\/span> result;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We immediately call <code>next()<\/code>, which runs the actual server function this middleware is attached to. We pass a <code>payloads<\/code> array in <code>sendContext<\/code>. This governs what gets sent <em>back<\/em> to the client callback (that&#8217;s how <code>.client<\/code> got the payloads array we just saw it looping through).<\/p>\n\n\n\n<p>Then we run through the revalidate payloads sent up from the client. The client sent them via <code>sendContext<\/code>, and we read them from the context object (<em>send<\/em> context, get it?). We then call all the server functions, and add to that payloads array.<\/p>\n\n\n\n<p>Here&#8217;s the entire middleware:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> refetchMiddleware = createMiddleware({ <span class=\"hljs-keyword\">type<\/span>: <span class=\"hljs-string\">\"function\"<\/span> })\n  .inputValidator(<span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">config<\/span>?: <span class=\"hljs-params\">RefetchMiddlewareConfig<\/span><\/span>) =&gt;<\/span> config)\n  .client(<span class=\"hljs-keyword\">async<\/span> ({ next, data }) =&gt; {\n    <span class=\"hljs-keyword\">const<\/span> { refetch = &#91;] } = data ?? {};\n\n    <span class=\"hljs-keyword\">const<\/span> router = <span class=\"hljs-keyword\">await<\/span> getRouterInstance();\n    <span class=\"hljs-keyword\">const<\/span> queryClient: QueryClient = router.options.context.queryClient;\n    <span class=\"hljs-keyword\">const<\/span> cache = queryClient.getQueryCache();\n\n    <span class=\"hljs-keyword\">const<\/span> revalidate: RevalidationPayload = {\n      refetch: &#91;],\n    };\n\n    refetch.forEach(<span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">key<\/span>: <span class=\"hljs-params\">QueryKey<\/span><\/span>) =&gt;<\/span> {\n      <span class=\"hljs-keyword\">const<\/span> entry = cache.find({ queryKey: key, exact: <span class=\"hljs-literal\">true<\/span> });\n      <span class=\"hljs-keyword\">if<\/span> (!entry) <span class=\"hljs-keyword\">return<\/span>;\n\n      <span class=\"hljs-keyword\">const<\/span> revalidatePayload: <span class=\"hljs-built_in\">any<\/span> = entry?.meta?.__revalidate ?? <span class=\"hljs-literal\">null<\/span>;\n\n      <span class=\"hljs-keyword\">if<\/span> (revalidatePayload) {\n        revalidate.refetch.push({\n          key,\n          fn: revalidatePayload.serverFn,\n          arg: revalidatePayload.arg,\n        });\n      }\n    });\n\n    <span class=\"hljs-keyword\">const<\/span> result = <span class=\"hljs-keyword\">await<\/span> next({\n      sendContext: {\n        revalidate,\n      },\n    });\n\n    <span class=\"hljs-comment\">\/\/ @ts-expect-error<\/span>\n    <span class=\"hljs-keyword\">for<\/span> (<span class=\"hljs-keyword\">const<\/span> entry of result.context?.payloads ?? &#91;]) {\n      queryClient.setQueryData(entry.key, entry.result);\n    }\n\n    <span class=\"hljs-keyword\">return<\/span> result;\n  })\n  .server(<span class=\"hljs-keyword\">async<\/span> ({ next, context }) =&gt; {\n    <span class=\"hljs-keyword\">const<\/span> result = <span class=\"hljs-keyword\">await<\/span> next({\n      sendContext: {\n        payloads: &#91;] <span class=\"hljs-keyword\">as<\/span> <span class=\"hljs-built_in\">any<\/span>&#91;],\n      },\n    });\n\n    <span class=\"hljs-keyword\">const<\/span> allPayloads = context.revalidate.refetch.map(<span class=\"hljs-function\"><span class=\"hljs-params\">refetchPayload<\/span> =&gt;<\/span> {\n      <span class=\"hljs-keyword\">return<\/span> {\n        key: refetchPayload.key,\n        result: refetchPayload.fn({ data: refetchPayload.arg }),\n      };\n    });\n\n    <span class=\"hljs-keyword\">for<\/span> (<span class=\"hljs-keyword\">const<\/span> refetchPayload of allPayloads) {\n      result.sendContext.payloads.push({\n        key: refetchPayload.key,\n        result: <span class=\"hljs-keyword\">await<\/span> refetchPayload.result,\n      });\n    }\n\n    <span class=\"hljs-keyword\">return<\/span> result;\n  });<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h2 class=\"wp-block-heading\" id=\"fixing-the-typescript-error\">Fixing the TypeScript Error<\/h2>\n\n\n\n<p>Why is this line invalid?<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-comment\">\/\/ @ts-expect-error<\/span>\n<span class=\"hljs-keyword\">for<\/span> (<span class=\"hljs-keyword\">const<\/span> entry of result.context?.payloads ?? &#91;]) {<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-12\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This line runs in the <code>.client<\/code> callback, <em>after<\/em> we call <code>next()<\/code>. Essentially, we&#8217;re trying to read properties sent back to the client, from the server (via the <code>sendContext<\/code> payload). This runs, and works properly. But why don&#8217;t the types line up?<\/p>\n\n\n\n<p>I covered this in my Middleware post linked above, but our server callback can see what gets sent to it from the client, but the reverse is not true. This knowledge just inherently does not go in both directions; the type inference cannot run backwards.<\/p>\n\n\n\n<p>The solution is simple: just break the middleware into two pieces, and make one of them a middleware dependency on the other.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-13\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> prelimRefetchMiddleware = createMiddleware({ <span class=\"hljs-keyword\">type<\/span>: <span class=\"hljs-string\">\"function\"<\/span> })\n  .inputValidator(<span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">config<\/span>?: <span class=\"hljs-params\">RefetchMiddlewareConfig<\/span><\/span>) =&gt;<\/span> config)\n  .client(<span class=\"hljs-keyword\">async<\/span> ({ next, data }) =&gt; {\n    <span class=\"hljs-keyword\">const<\/span> { refetch = &#91;] } = data ?? {};\n\n    <span class=\"hljs-keyword\">const<\/span> router = <span class=\"hljs-keyword\">await<\/span> getRouterInstance();\n    <span class=\"hljs-keyword\">const<\/span> queryClient: QueryClient = router.options.context.queryClient;\n\n    <span class=\"hljs-comment\">\/\/ same<\/span>\n    <span class=\"hljs-comment\">\/\/ as<\/span>\n    <span class=\"hljs-comment\">\/\/ before<\/span>\n\n    <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-keyword\">await<\/span> next({\n      sendContext: {\n        revalidate,\n      },\n    });\n\n    <span class=\"hljs-comment\">\/\/ those last few lines are removed<\/span>\n  })\n  .server(<span class=\"hljs-keyword\">async<\/span> ({ next, context }) =&gt; {\n    <span class=\"hljs-keyword\">const<\/span> result = <span class=\"hljs-keyword\">await<\/span> next({\n      sendContext: {\n        payloads: &#91;] <span class=\"hljs-keyword\">as<\/span> <span class=\"hljs-built_in\">any<\/span>&#91;],\n      },\n    });\n\n    <span class=\"hljs-comment\">\/\/ exactly the same as before<\/span>\n\n    <span class=\"hljs-keyword\">return<\/span> result;\n  });\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> refetchMiddleware = createMiddleware({ <span class=\"hljs-keyword\">type<\/span>: <span class=\"hljs-string\">\"function\"<\/span> })\n  .middleware(&#91;prelimRefetchMiddleware]) <span class=\"hljs-comment\">\/\/ &lt;-------- connect them!<\/span>\n  .client(<span class=\"hljs-keyword\">async<\/span> ({ next }) =&gt; {\n    <span class=\"hljs-keyword\">const<\/span> result = <span class=\"hljs-keyword\">await<\/span> next();\n\n    <span class=\"hljs-keyword\">const<\/span> router = <span class=\"hljs-keyword\">await<\/span> getRouterInstance();\n    <span class=\"hljs-keyword\">const<\/span> queryClient: QueryClient = router.options.context.queryClient;\n\n    <span class=\"hljs-comment\">\/\/ and here's those last few lines we removed from above<\/span>\n    <span class=\"hljs-keyword\">for<\/span> (<span class=\"hljs-keyword\">const<\/span> entry of result.context?.payloads ?? &#91;]) {\n      queryClient.setQueryData(entry.key, entry.result);\n    }\n\n    <span class=\"hljs-keyword\">return<\/span> result;\n  });<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-13\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>It&#8217;s the same as before, except everything in the <code>.client<\/code> callback <em>after<\/em> the call to <code>next()<\/code> is now in its own middleware. The rest is in a different middleware, which is an input to this one. Now when we call <code>next<\/code> in <code>refetchMiddleware<\/code>, TypeScript is able to see the data that&#8217;s been sent down from the server, since that was done in <code>prelimRefetchMiddleware<\/code>, which is an <em>input<\/em> to this middleware, which allows TypeScript to fully see the flow of types.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"wiring-it-up\">Wiring it Up<\/h2>\n\n\n\n<p>Now we can take our server function for updating an epic, remove the refecthes, and add our refetch middleware.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-14\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> updateEpic = createServerFn({ method: <span class=\"hljs-string\">\"POST\"<\/span> })\n  .middleware(&#91;refetchMiddleware])\n  .inputValidator(<span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">obj<\/span>: { <span class=\"hljs-params\">id<\/span>: <span class=\"hljs-params\">number<\/span>; <span class=\"hljs-params\">name<\/span>: <span class=\"hljs-params\">string<\/span> }<\/span>) =&gt;<\/span> obj)\n  .handler(<span class=\"hljs-keyword\">async<\/span> ({ data }) =&gt; {\n    <span class=\"hljs-keyword\">await<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Promise<\/span>(<span class=\"hljs-function\"><span class=\"hljs-params\">resolve<\/span> =&gt;<\/span> setTimeout(resolve, <span class=\"hljs-number\">1000<\/span> * <span class=\"hljs-built_in\">Math<\/span>.random()));\n    <span class=\"hljs-keyword\">await<\/span> db.update(epicsTable).set({ name: data.name }).where(eq(epicsTable.id, data.id));\n  });<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-14\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We set it up to call from our React component with the <code>useServerFn<\/code> hook, which handles things like errors and redirects automatically.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-15\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> runSave = useServerFn(updateEpic);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-15\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Remember when I said that inputs to middleware are automatically merged with inputs to the underlying server function? We can see that first hand when we call the server function.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"272\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/img8.png?resize=1024%2C272&#038;ssl=1\" alt=\"A code snippet showing a function handleSaveFinal, where an input value is being saved and a runSave function is called with an object containing id and name properties.\" class=\"wp-image-8393\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/img8.png?resize=1024%2C272&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/img8.png?resize=300%2C80&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/img8.png?resize=768%2C204&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/img8.png?resize=1536%2C407&amp;ssl=1 1536w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/img8.png?w=1704&amp;ssl=1 1704w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">(<code>unknown[]<\/code> is the correct type for react-query query keys)<\/figcaption><\/figure>\n\n\n\n<p>Now we can call it, and specify the queries we want refetched.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-16\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">await<\/span> runSave({\n  data: {\n    id: epic.id,\n    name: newValue,\n    refetch: &#91;\n      &#91;<span class=\"hljs-string\">\"epics\"<\/span>, <span class=\"hljs-string\">\"list\"<\/span>, <span class=\"hljs-number\">1<\/span>],\n      &#91;<span class=\"hljs-string\">\"epics\"<\/span>, <span class=\"hljs-string\">\"list\"<\/span>, <span class=\"hljs-string\">\"summary\"<\/span>],\n    ],\n  },\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-16\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>When we run it, it works. Both the list of epics, and also the summary list correctly update with our changes, <em>without<\/em> any new requests in the network tab. When testing single flight mutations, we&#8217;re not really looking for <em>something<\/em> to indicate that it worked, but rather a <em>lack of<\/em> new network requests, for updated data.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"improving-things\">Improving Things<\/h2>\n\n\n\n<p>Query keys are hierarchical in react-query. You might already be familiar with this. Normally, when updating data, it would be common to do something like:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-17\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\">queryClient.invalidateQueries({ queryKey: &#91;<span class=\"hljs-string\">\"epics\"<\/span>, <span class=\"hljs-string\">\"list\"<\/span>] });<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-17\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Which refetches <em>any<\/em> queries whose key <em>starts with<\/em> <code>[\"epics\", \"list\"]<\/code>. Can we do something similar in our middleware? Like, just pass in that key prefix, and have it find, and refetch whatever&#8217;s there?<\/p>\n\n\n\n<p>Let&#8217;s do it!<\/p>\n\n\n\n<p>Getting the matching keys will be <em>slightly<\/em> more complicated. Each key we pass up will potentially be a key prefix, matching multiple entries, so we\u2019ll use <code>flatMap<\/code> to find all matches, with the nifty <code>cache.findAll<\/code> method.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-18\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> allQueriesFound = refetch.flatMap(\n  <span class=\"hljs-function\"><span class=\"hljs-params\">k<\/span> =&gt;<\/span> cache.findAll({ queryKey: k, exact: <span class=\"hljs-literal\">false<\/span> })\n);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-18\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Now we loop them, and do the same thing as before.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-19\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> allQueriesFound = refetch.flatMap(\n  <span class=\"hljs-function\"><span class=\"hljs-params\">k<\/span> =&gt;<\/span> cache.findAll({ queryKey: k, exact: <span class=\"hljs-literal\">false<\/span> })\n);\n\nallQueriesFound.forEach(<span class=\"hljs-function\"><span class=\"hljs-params\">entry<\/span> =&gt;<\/span> {\n  <span class=\"hljs-keyword\">const<\/span> revalidatePayload: <span class=\"hljs-built_in\">any<\/span> = entry?.meta?.__revalidate ?? <span class=\"hljs-literal\">null<\/span>;\n\n  <span class=\"hljs-keyword\">if<\/span> (revalidatePayload) {\n    revalidate.refetch.push({\n      key: entry.queryKey,\n      fn: revalidatePayload.serverFn,\n      arg: revalidatePayload.arg,\n    });\n  }\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-19\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This works!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"going-deeper\">Going Deeper<\/h2>\n\n\n\n<p>Our solution still isn&#8217;t ideal. What if we page around in our epics page (up to page 2, up to page 3, then back down). Our solution will find page 1, and our summary query, but also pages 2 and 3, since they&#8217;re now in cache. But pages 2 and 3 aren&#8217;t really active, and we shouldn&#8217;t refetch them, since they&#8217;re not even being displayed.<\/p>\n\n\n\n<p>Let&#8217;s change our code to only refetch active queries. Detecting whether a query entry is actually active is as simple as adding the <code>type<\/code> argument to <code>findAll<\/code>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-20\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\">cache.findAll({ queryKey: key, exact: <span class=\"hljs-literal\">false<\/span>, <span class=\"hljs-keyword\">type<\/span>: <span class=\"hljs-string\">\"active\"<\/span> });<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-20\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Our code now looks like this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-21\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> allQueriesFound = refetch.flatMap(<span class=\"hljs-function\"><span class=\"hljs-params\">key<\/span> =&gt;<\/span> cache.findAll({ queryKey: key, exact: <span class=\"hljs-literal\">false<\/span>, <span class=\"hljs-keyword\">type<\/span>: <span class=\"hljs-string\">\"active\"<\/span> }));\n\nallQueriesFound.forEach(<span class=\"hljs-function\"><span class=\"hljs-params\">entry<\/span> =&gt;<\/span> {\n  <span class=\"hljs-keyword\">const<\/span> revalidatePayload: <span class=\"hljs-built_in\">any<\/span> = entry?.meta?.__revalidate ?? <span class=\"hljs-literal\">null<\/span>;\n\n  <span class=\"hljs-keyword\">if<\/span> (revalidatePayload) {\n    revalidate.refetch.push({\n      key: entry.queryKey,\n      fn: revalidatePayload.serverFn,\n      arg: revalidatePayload.arg,\n    });\n  }\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-21\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h2 class=\"wp-block-heading\" id=\"even-deeper\">Even Deeper<\/h2>\n\n\n\n<p>This works. But when you think about it, those other, inactive queries should probably be invalidated. We don&#8217;t want to waste resources refetching them immediately, since they&#8217;re not being used; but if the user were to browse back to those pages, we probably want the data refetched. Tanstack Query makes that easy, via the <code>invalidateQueries<\/code> method.<\/p>\n\n\n\n<p>We&#8217;ll add this to the client callback of the middleware we feed into.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-22\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\">data?.refetch.forEach(<span class=\"hljs-function\"><span class=\"hljs-params\">key<\/span> =&gt;<\/span> {\n  queryClient.invalidateQueries({ queryKey: key, exact: <span class=\"hljs-literal\">false<\/span>, <span class=\"hljs-keyword\">type<\/span>: <span class=\"hljs-string\">\"inactive\"<\/span>, refetchType: <span class=\"hljs-string\">\"none\"<\/span> });\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-22\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Loop the query keys we passed in, and invalidate any of the inactive queries (the active ones have already been refetched), but without refetching them.<\/p>\n\n\n\n<p>Here&#8217;s our entire, updated middleware.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-23\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> prelimRefetchMiddleware = createMiddleware({ <span class=\"hljs-keyword\">type<\/span>: <span class=\"hljs-string\">\"function\"<\/span> })\n  .inputValidator(<span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">config<\/span>?: <span class=\"hljs-params\">RefetchMiddlewareConfig<\/span><\/span>) =&gt;<\/span> config)\n  .client(<span class=\"hljs-keyword\">async<\/span> ({ next, data }) =&gt; {\n    <span class=\"hljs-keyword\">const<\/span> { refetch = &#91;] } = data ?? {};\n\n    <span class=\"hljs-keyword\">const<\/span> router = <span class=\"hljs-keyword\">await<\/span> getRouterInstance();\n    <span class=\"hljs-keyword\">const<\/span> queryClient: QueryClient = router.options.context.queryClient;\n    <span class=\"hljs-keyword\">const<\/span> cache = queryClient.getQueryCache();\n\n    <span class=\"hljs-keyword\">const<\/span> revalidate: RevalidationPayload = {\n      refetch: &#91;],\n    };\n\n    <span class=\"hljs-keyword\">const<\/span> allQueriesFound = refetch.flatMap(<span class=\"hljs-function\"><span class=\"hljs-params\">key<\/span> =&gt;<\/span> cache.findAll({ queryKey: key, exact: <span class=\"hljs-literal\">false<\/span>, <span class=\"hljs-keyword\">type<\/span>: <span class=\"hljs-string\">\"active\"<\/span> }));\n\n    allQueriesFound.forEach(<span class=\"hljs-function\"><span class=\"hljs-params\">entry<\/span> =&gt;<\/span> {\n      <span class=\"hljs-keyword\">const<\/span> revalidatePayload: <span class=\"hljs-built_in\">any<\/span> = entry?.meta?.__revalidate ?? <span class=\"hljs-literal\">null<\/span>;\n\n      <span class=\"hljs-keyword\">if<\/span> (revalidatePayload) {\n        revalidate.refetch.push({\n          key: entry.queryKey,\n          fn: revalidatePayload.serverFn,\n          arg: revalidatePayload.arg,\n        });\n      }\n    });\n\n    <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-keyword\">await<\/span> next({\n      sendContext: {\n        revalidate,\n      },\n    });\n  })\n  .server(<span class=\"hljs-keyword\">async<\/span> ({ next, context }) =&gt; {\n    <span class=\"hljs-keyword\">const<\/span> result = <span class=\"hljs-keyword\">await<\/span> next({\n      sendContext: {\n        payloads: &#91;] <span class=\"hljs-keyword\">as<\/span> <span class=\"hljs-built_in\">any<\/span>&#91;],\n      },\n    });\n\n    <span class=\"hljs-keyword\">const<\/span> allPayloads = context.revalidate.refetch.map(<span class=\"hljs-function\"><span class=\"hljs-params\">refetchPayload<\/span> =&gt;<\/span> {\n      <span class=\"hljs-keyword\">return<\/span> {\n        key: refetchPayload.key,\n        result: refetchPayload.fn({ data: refetchPayload.arg }),\n      };\n    });\n\n    <span class=\"hljs-keyword\">for<\/span> (<span class=\"hljs-keyword\">const<\/span> refetchPayload of allPayloads) {\n      result.sendContext.payloads.push({\n        key: refetchPayload.key,\n        result: <span class=\"hljs-keyword\">await<\/span> refetchPayload.result,\n      });\n    }\n\n    <span class=\"hljs-keyword\">return<\/span> result;\n  });\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> refetchMiddleware = createMiddleware({ <span class=\"hljs-keyword\">type<\/span>: <span class=\"hljs-string\">\"function\"<\/span> })\n  .middleware(&#91;prelimRefetchMiddleware])\n  .client(<span class=\"hljs-keyword\">async<\/span> ({ data, next }) =&gt; {\n    <span class=\"hljs-keyword\">const<\/span> result = <span class=\"hljs-keyword\">await<\/span> next();\n\n    <span class=\"hljs-keyword\">const<\/span> router = <span class=\"hljs-keyword\">await<\/span> getRouterInstance();\n    <span class=\"hljs-keyword\">const<\/span> queryClient: QueryClient = router.options.context.queryClient;\n\n    <span class=\"hljs-keyword\">for<\/span> (<span class=\"hljs-keyword\">const<\/span> entry of result.context?.payloads ?? &#91;]) {\n      queryClient.setQueryData(entry.key, entry.result, { updatedAt: <span class=\"hljs-built_in\">Date<\/span>.now() });\n    }\n\n    data?.refetch.forEach(<span class=\"hljs-function\"><span class=\"hljs-params\">key<\/span> =&gt;<\/span> {\n      queryClient.invalidateQueries({ queryKey: key, exact: <span class=\"hljs-literal\">false<\/span>, <span class=\"hljs-keyword\">type<\/span>: <span class=\"hljs-string\">\"inactive\"<\/span>, refetchType: <span class=\"hljs-string\">\"none\"<\/span> });\n    });\n\n    <span class=\"hljs-keyword\">return<\/span> result;\n  });<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-23\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We tell TanStack Query to invalidate (but not refetch) any inactive queries matching our key.<\/p>\n\n\n\n<p>This works perfectly. If we browse up to pages 2 and 3, and then back to page 1, then edit a todo, we do in fact see our list, and summary list update. If we then page back to page 2, and 3, we&#8217;ll see network requests fire to get fresh data.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"icing-on-the-cake\">Icing on the Cake<\/h2>\n\n\n\n<p>Remember when we added the server function, and the arg it takes to our query options?<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-24\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> epicsQueryOptions = <span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">page<\/span>: <span class=\"hljs-params\">number<\/span><\/span>) =&gt;<\/span> {\n  <span class=\"hljs-keyword\">return<\/span> queryOptions({\n    queryKey: &#91;<span class=\"hljs-string\">\"epics\"<\/span>, <span class=\"hljs-string\">\"list\"<\/span>, page],\n    queryFn: <span class=\"hljs-keyword\">async<\/span> () =&gt; {\n      <span class=\"hljs-keyword\">const<\/span> result = <span class=\"hljs-keyword\">await<\/span> getEpicsList({ data: page });\n      <span class=\"hljs-keyword\">return<\/span> result;\n    },\n    staleTime: <span class=\"hljs-number\">1000<\/span> * <span class=\"hljs-number\">60<\/span> * <span class=\"hljs-number\">5<\/span>,\n    gcTime: <span class=\"hljs-number\">1000<\/span> * <span class=\"hljs-number\">60<\/span> * <span class=\"hljs-number\">5<\/span>,\n    meta: {\n      __revalidate: {\n        serverFn: getEpicsList,\n        arg: page,\n      },\n    },\n  });\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-24\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>I briefly noted that it was a bit gross to duplicate the server function, and arg in both our <code>meta<\/code> object, and our <code>queryFn<\/code>. Let&#8217;s fix this.<\/p>\n\n\n\n<p>Let&#8217;s start with the simplest possible helper to remove this duplication, and as before, iterate on it.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-25\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">refetchedQueryOptions<\/span>(<span class=\"hljs-params\">queryKey: QueryKey, serverFn: <span class=\"hljs-built_in\">any<\/span>, arg?: <span class=\"hljs-built_in\">any<\/span><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> queryKeyToUse = &#91;...queryKey];\n  <span class=\"hljs-keyword\">if<\/span> (arg != <span class=\"hljs-literal\">null<\/span>) {\n    queryKeyToUse.push(arg);\n  }\n  <span class=\"hljs-keyword\">return<\/span> queryOptions({\n    queryKey: queryKeyToUse,\n    queryFn: <span class=\"hljs-keyword\">async<\/span> () =&gt; {\n      <span class=\"hljs-keyword\">return<\/span> serverFn({ data: arg });\n    },\n    meta: {\n      __revalidate: {\n        serverFn,\n        arg,\n      },\n    },\n  });\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-25\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>It&#8217;s just a simple helper that takes in your query key, server function and arg, and returns back some of our query options: our <code>queryKey<\/code> (to which we add whatever argument we need for the server function), the <code>queryFn<\/code> which calls the server function, and our meta object.<\/p>\n\n\n\n<p>Our epics list query now looks like this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-26\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> epicsQueryOptions = <span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">page<\/span>: <span class=\"hljs-params\">number<\/span><\/span>) =&gt;<\/span> {\n  <span class=\"hljs-keyword\">return<\/span> queryOptions({\n    ...refetchedQueryOptions(&#91;<span class=\"hljs-string\">\"epics\"<\/span>, <span class=\"hljs-string\">\"list\"<\/span>], getEpicsList, page),\n    staleTime: <span class=\"hljs-number\">1000<\/span> * <span class=\"hljs-number\">60<\/span> * <span class=\"hljs-number\">5<\/span>,\n    gcTime: <span class=\"hljs-number\">1000<\/span> * <span class=\"hljs-number\">60<\/span> * <span class=\"hljs-number\">5<\/span>,\n  });\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-26\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This works, but it&#8217;s not great. We have <code>any<\/code> types everywhere, which means the argument we pass to our server function is never type checked. Even worse, the return value of our <code>queryFn<\/code> is not type checked, which means our queries (like this very epics list query) now return <code>any<\/code>.<\/p>\n\n\n\n<p>Let&#8217;s add some typings. Server functions are just functions. They take a single object argument, and if the server function has defined an input, then that argument will have a data property for that argument. That&#8217;s a lot of words to say what we already know. When we call a server function, we pass our argument like this<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-27\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> result = <span class=\"hljs-keyword\">await<\/span> runSaveSimple({\n  data: {\n    id: epic.id,\n    name: newValue,\n  },\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-27\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>How&#8217;s this for a second draft?<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-28\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">refetchedQueryOptions<\/span>&lt;<span class=\"hljs-title\">T<\/span> <span class=\"hljs-title\">extends<\/span> (<span class=\"hljs-params\">arg: { data: <span class=\"hljs-built_in\">any<\/span> }<\/span>) =&gt; <span class=\"hljs-title\">Promise<\/span>&lt;<span class=\"hljs-title\">any<\/span>&gt;&gt;(<span class=\"hljs-params\">\n  queryKey: QueryKey,\n  serverFn: T,\n  arg: Parameters&lt;T&gt;&#91;0]&#91;\"data\"],\n<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> queryKeyToUse = &#91;...queryKey];\n  <span class=\"hljs-keyword\">if<\/span> (arg != <span class=\"hljs-literal\">null<\/span>) {\n    queryKeyToUse.push(arg);\n  }\n  <span class=\"hljs-keyword\">return<\/span> queryOptions({\n    queryKey: queryKeyToUse,\n    queryFn: <span class=\"hljs-keyword\">async<\/span> (): <span class=\"hljs-built_in\">Promise<\/span>&lt;Awaited&lt;ReturnType&lt;T&gt;&gt;&gt; =&gt; {\n      <span class=\"hljs-keyword\">return<\/span> serverFn({ data: arg });\n    },\n    meta: {\n      __revalidate: {\n        serverFn,\n        arg,\n      },\n    },\n  });\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-28\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We&#8217;ve constrained our server function to an async function which takes a <code>data<\/code> prop on its object arg, and we&#8217;ve used that to statically type the argument. This is good, but we get an error when we use this on server functions which have no arguments.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-29\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\">...refetchedQueryOptions(&#91;<span class=\"hljs-string\">\"epics\"<\/span>, <span class=\"hljs-string\">\"list\"<\/span>, <span class=\"hljs-string\">\"summary\"<\/span>], getEpicsSummary)\n<span class=\"hljs-comment\">\/\/ Expected 3 arguments, but got 2.<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-29\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Adding an <code>undefined<\/code> does fix this, and everything works.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-30\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\">...refetchedQueryOptions(&#91;<span class=\"hljs-string\">\"epics\"<\/span>, <span class=\"hljs-string\">\"list\"<\/span>, <span class=\"hljs-string\">\"summary\"<\/span>], getEpicsSummary, <span class=\"hljs-literal\">undefined<\/span>),<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-30\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>If you&#8217;re normal, you&#8217;re probably happy with that. And you should be. But if you&#8217;re weird like me, you might wonder if you can&#8217;t make it perfect. Ideally it would be cool if we could pass a statically typed argument when using a server function that takes an input, and when using a server function with no input, pass nothing.<\/p>\n\n\n\n<p>TypeScript has a feature exactly for this: <strong>overloaded functions<\/strong>.<\/p>\n\n\n\n<p>This post is already far too long, so I&#8217;ll post the code, and leave deciphering it as an exercise for the reader (and likely a future blog post).<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-31\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">import<\/span> { QueryKey, queryOptions } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@tanstack\/react-query\"<\/span>;\n\n<span class=\"hljs-keyword\">type<\/span> AnyAsyncFn = <span class=\"hljs-function\">(<span class=\"hljs-params\">...<span class=\"hljs-params\">args<\/span>: <span class=\"hljs-params\">any<\/span>&#91;]<\/span>) =&gt;<\/span> <span class=\"hljs-built_in\">Promise<\/span>&lt;<span class=\"hljs-built_in\">any<\/span>&gt;;\n\n<span class=\"hljs-keyword\">type<\/span> ServerFnArgs&lt;TFn <span class=\"hljs-keyword\">extends<\/span> AnyAsyncFn&gt; = Parameters&lt;TFn&gt;&#91;<span class=\"hljs-number\">0<\/span>] <span class=\"hljs-keyword\">extends<\/span> infer TRootArgs\n  ? TRootArgs <span class=\"hljs-keyword\">extends<\/span> { data: infer TResult }\n    ? TResult\n    : <span class=\"hljs-literal\">undefined<\/span>\n  : never;\n\n<span class=\"hljs-keyword\">type<\/span> ServerFnHasArgs&lt;TFn <span class=\"hljs-keyword\">extends<\/span> AnyAsyncFn&gt; = ServerFnArgs&lt;TFn&gt; <span class=\"hljs-keyword\">extends<\/span> infer U ? (U <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-literal\">undefined<\/span> ? <span class=\"hljs-literal\">false<\/span> : <span class=\"hljs-literal\">true<\/span>) : <span class=\"hljs-literal\">false<\/span>;\n\n<span class=\"hljs-keyword\">type<\/span> ServerFnWithArgs&lt;TFn <span class=\"hljs-keyword\">extends<\/span> AnyAsyncFn&gt; = ServerFnHasArgs&lt;TFn&gt; <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-literal\">true<\/span> ? TFn : never;\n<span class=\"hljs-keyword\">type<\/span> ServerFnWithoutArgs&lt;TFn <span class=\"hljs-keyword\">extends<\/span> AnyAsyncFn&gt; = ServerFnHasArgs&lt;TFn&gt; <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-literal\">false<\/span> ? TFn : never;\n\n<span class=\"hljs-keyword\">type<\/span> RefetchQueryOptions&lt;T&gt; = {\n  queryKey: QueryKey;\n  queryFn?: <span class=\"hljs-function\">(<span class=\"hljs-params\">_: <span class=\"hljs-params\">any<\/span><\/span>) =&gt;<\/span> <span class=\"hljs-built_in\">Promise<\/span>&lt;T&gt;;\n  meta?: <span class=\"hljs-built_in\">any<\/span>;\n};\n\n<span class=\"hljs-keyword\">type<\/span> ValidateServerFunction&lt;Provided, Expected&gt; = Provided <span class=\"hljs-keyword\">extends<\/span> Expected ? Provided : <span class=\"hljs-string\">\"This server function requires an argument!\"<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">refetchedQueryOptions<\/span>&lt;<span class=\"hljs-title\">TFn<\/span> <span class=\"hljs-title\">extends<\/span> <span class=\"hljs-title\">AnyAsyncFn<\/span>&gt;(<span class=\"hljs-params\">\n  queryKey: QueryKey,\n  serverFn: ServerFnWithArgs&lt;TFn&gt;,\n  arg: Parameters&lt;TFn&gt;&#91;0]&#91;\"data\"],\n<\/span>): <span class=\"hljs-title\">RefetchQueryOptions<\/span>&lt;<span class=\"hljs-title\">Awaited<\/span>&lt;<span class=\"hljs-title\">ReturnType<\/span>&lt;<span class=\"hljs-title\">TFn<\/span>&gt;&gt;&gt;<\/span>;\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">refetchedQueryOptions<\/span>&lt;<span class=\"hljs-title\">TFn<\/span> <span class=\"hljs-title\">extends<\/span> <span class=\"hljs-title\">AnyAsyncFn<\/span>&gt;(<span class=\"hljs-params\">\n  queryKey: QueryKey,\n  serverFn: ValidateServerFunction&lt;TFn, ServerFnWithoutArgs&lt;TFn&gt;&gt;,\n<\/span>): <span class=\"hljs-title\">RefetchQueryOptions<\/span>&lt;<span class=\"hljs-title\">Awaited<\/span>&lt;<span class=\"hljs-title\">ReturnType<\/span>&lt;<span class=\"hljs-title\">TFn<\/span>&gt;&gt;&gt;<\/span>;\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">refetchedQueryOptions<\/span>&lt;<span class=\"hljs-title\">TFn<\/span> <span class=\"hljs-title\">extends<\/span> <span class=\"hljs-title\">AnyAsyncFn<\/span>&gt;(<span class=\"hljs-params\">\n  queryKey: QueryKey,\n  serverFn: ServerFnWithoutArgs&lt;TFn&gt; | ServerFnWithArgs&lt;TFn&gt;,\n  arg?: Parameters&lt;TFn&gt;&#91;0]&#91;\"data\"],\n<\/span>): <span class=\"hljs-title\">RefetchQueryOptions<\/span>&lt;<span class=\"hljs-title\">Awaited<\/span>&lt;<span class=\"hljs-title\">ReturnType<\/span>&lt;<span class=\"hljs-title\">TFn<\/span>&gt;&gt;&gt; <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> queryKeyToUse = &#91;...queryKey];\n  <span class=\"hljs-keyword\">if<\/span> (arg != <span class=\"hljs-literal\">null<\/span>) {\n    queryKeyToUse.push(arg);\n  }\n  <span class=\"hljs-keyword\">return<\/span> queryOptions({\n    queryKey: queryKeyToUse,\n    queryFn: <span class=\"hljs-keyword\">async<\/span> () =&gt; {\n      <span class=\"hljs-keyword\">return<\/span> serverFn({ data: arg });\n    },\n    meta: {\n      __revalidate: {\n        serverFn,\n        arg,\n      },\n    },\n  });\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-31\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>With this in place we can now call it with server functions that take an argument.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-32\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> epicsQueryOptions = <span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">page<\/span>: <span class=\"hljs-params\">number<\/span><\/span>) =&gt;<\/span> {\n  <span class=\"hljs-keyword\">return<\/span> queryOptions({\n    ...refetchedQueryOptions(&#91;<span class=\"hljs-string\">\"epics\"<\/span>, <span class=\"hljs-string\">\"list\"<\/span>], getEpicsList, page),\n    staleTime: <span class=\"hljs-number\">1000<\/span> * <span class=\"hljs-number\">60<\/span> * <span class=\"hljs-number\">5<\/span>,\n    gcTime: <span class=\"hljs-number\">1000<\/span> * <span class=\"hljs-number\">60<\/span> * <span class=\"hljs-number\">5<\/span>,\n  });\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-32\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The parameter is now checked. It errors with the wrong type.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-33\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\">...refetchedQueryOptions(&#91;<span class=\"hljs-string\">\"epics\"<\/span>, <span class=\"hljs-string\">\"list\"<\/span>], getEpicsList, <span class=\"hljs-string\">\"\"<\/span>)\n<span class=\"hljs-comment\">\/\/ Argument of type 'string' is not assignable to parameter of type 'number'.<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-33\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>It errors if you pass no argument as well.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-34\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\">...refetchedQueryOptions(&#91;<span class=\"hljs-string\">\"epics\"<\/span>, <span class=\"hljs-string\">\"list\"<\/span>], getEpicsList)\n<span class=\"hljs-comment\">\/\/ Argument of type 'RequiredFetcher&lt;undefined, (page: number) =&gt; number, Promise&lt;{ id: number; name: string; }&#91;]&gt;&gt;' is not assignable to parameter of type '\"This server function requires an argument!\"'.<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-34\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>That last error isn&#8217;t the clearest, but if you read to the end you get a pretty solid hint as to what&#8217;s wrong, thanks to this dandy little helper.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-35\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">type<\/span> ValidateServerFunction&lt;Provided, Expected&gt; = Provided <span class=\"hljs-keyword\">extends<\/span> Expected ? Provided : <span class=\"hljs-string\">\"This server function requires an argument!\"<\/span>;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-35\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>And it works with a server function that takes no arguments. Again, a full explanation of this TypeScript will have to wait for a future post.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"concluding-thoughts\">Concluding Thoughts<\/h2>\n\n\n\n<p>Single flight mutations are a great tool for speeding up updates within your web app, particularly when it&#8217;s a performance boost to avoid follow-up network requests for data after an initial mutation. Hopefully this post has shown you the tools needed to put it all together.<\/p>\n\n\n<div class=\"box article-series\">\n  <header>\n    <h3 class=\"article-series-header\">Article Series<\/h3>\n  <\/header>\n  <div class=\"box-content\">\n            <ol>\n                      <li>\n              <a href=\"https:\/\/frontendmasters.com\/blog\/single-flight-mutations-in-tanstack-start-part-1\/\">Single Flight Mutations in TanStack Start: Part 1<\/a>\n            <\/li>\n                      <li>\n              <a href=\"https:\/\/frontendmasters.com\/blog\/single-flight-mutations-in-tanstack-start-part-2\/\">Single Flight Mutations in TanStack Start: Part 2<\/a>\n            <\/li>\n                  <\/ol>\n        <\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>This post introduces a middleware approach that allows efficient data refetching. The middleware enables the attachment of query keys and server functions, enhancing scalability and flexibility.<\/p>\n","protected":false},"author":21,"featured_media":8372,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[174,3,240,184],"class_list":["post-8384","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-data","tag-javascript","tag-tanstack","tag-typescript"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/tanstack-round-trip.jpg?fit=2000%2C1200&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/8384","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/users\/21"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=8384"}],"version-history":[{"count":13,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/8384\/revisions"}],"predecessor-version":[{"id":8417,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/8384\/revisions\/8417"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/8372"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=8384"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=8384"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=8384"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}