{"id":4492,"date":"2024-11-21T13:11:14","date_gmt":"2024-11-21T18:11:14","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=4492"},"modified":"2024-11-21T13:11:15","modified_gmt":"2024-11-21T18:11:15","slug":"tanstack-router-data-loading-2","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/tanstack-router-data-loading-2\/","title":{"rendered":"Loading Data with TanStack Router: react-query"},"content":{"rendered":"\n<p><a href=\"https:\/\/tanstack.com\/query\/latest\">TanStack Query<\/a>, commonly referred to as react-query, is an incredibly popular tool for managing client-side querying. You could create an entire course on react-query, and people have, but here we\u2019re going to keep it brief so you can quickly get going.<\/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\/introducing-tanstack-router\/\">Introducing TanStack Router<\/a>\n            <\/li>\n                      <li>\n              <a href=\"https:\/\/frontendmasters.com\/blog\/tanstack-router-data-loading-1\/\">Loading Data with TanStack Router: Getting Going<\/a>\n            <\/li>\n                      <li>\n              <a href=\"https:\/\/frontendmasters.com\/blog\/tanstack-router-data-loading-2\/\">Loading Data with TanStack Router: react-query<\/a>\n            <\/li>\n                  <\/ol>\n        <\/div>\n<\/div>\n\n\n\n<p>Essentially, react-query allows us to write code like this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">const<\/span> { data, isLoading } = useQuery({\n  <span class=\"hljs-attr\">queryKey<\/span>: &#91;<span class=\"hljs-string\">\"task\"<\/span>, taskId],\n  <span class=\"hljs-attr\">queryFn<\/span>: <span class=\"hljs-keyword\">async<\/span> () =&gt; {\n    <span class=\"hljs-keyword\">return<\/span> fetchJson(<span class=\"hljs-string\">\"\/api\/tasks\/\"<\/span> + taskId);\n  },\n  <span class=\"hljs-attr\">staleTime<\/span>: <span class=\"hljs-number\">1000<\/span> * <span class=\"hljs-number\">60<\/span> * <span class=\"hljs-number\">2<\/span>,\n  <span class=\"hljs-attr\">gcTime<\/span>: <span class=\"hljs-number\">1000<\/span> * <span class=\"hljs-number\">60<\/span> * <span class=\"hljs-number\">5<\/span>,\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The <code>queryKey<\/code> does what it sounds like: it lets you identify any particular key for a query. As the key changes, react-query is smart enough to re-run the query, which is contained in the&nbsp;<code>queryFn<\/code>&nbsp;property. As these queries come in, TanStack tracks them in a client-side cache, along with properties like&nbsp;<code>staleTime<\/code>&nbsp;and&nbsp;<code>gcTime<\/code>, which mean the same thing as they do in TanStack Router. These tools are built by the same people, after all.<\/p>\n\n\n\n<p>There&#8217;s also a\u00a0<code>useSuspenseQuery<\/code>\u00a0hook which is the same idea, except instead of giving you an isLoading value, it relies on Suspense, and lets you handle loading state via Suspense boundaries.<\/p>\n\n\n\n<p>This barely scratches the surface of Query. If you\u2019ve never used it before, be sure to check out\u00a0<a href=\"https:\/\/tanstack.com\/query\/latest\">the docs<\/a>.<\/p>\n\n\n\n<p>We&#8217;ll move on and cover the setup and integration with Router, but we&#8217;ll stay high level to keep this post a manageable length.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"setup\">Setup<\/h2>\n\n\n\n<p>We need to wrap our entire app with a&nbsp;<code>QueryClientProvider<\/code>&nbsp;which injects a queryClient (and cache) into our application tree. Putting it around the&nbsp;<code>RouterProvider<\/code>&nbsp;we already have is as good a place as any.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">const<\/span> queryClient = <span class=\"hljs-keyword\">new<\/span> QueryClient();\n\n<span class=\"hljs-keyword\">const<\/span> Main: FC = <span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n  <span class=\"hljs-keyword\">return<\/span> (\n    <span class=\"xml\"><span class=\"hljs-tag\">&lt;&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">QueryClientProvider<\/span> <span class=\"hljs-attr\">client<\/span>=<span class=\"hljs-string\">{queryClient}<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">RouterProvider<\/span> <span class=\"hljs-attr\">router<\/span>=<span class=\"hljs-string\">{router}<\/span> <span class=\"hljs-attr\">context<\/span>=<span class=\"hljs-string\">{{<\/span> <span class=\"hljs-attr\">queryClient<\/span> }} \/&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">QueryClientProvider<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">TanStackRouterDevtools<\/span> <span class=\"hljs-attr\">router<\/span>=<span class=\"hljs-string\">{router}<\/span> \/&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/&gt;<\/span><\/span>\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\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Recall from before that we also passed our queryClient to our Router&#8217;s context like this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">const<\/span> router = createRouter({ \n  routeTree, \n  <span class=\"hljs-attr\">context<\/span>: { queryClient }\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>And:<\/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\">type<\/span> MyRouterContext = {\n  queryClient: QueryClient;\n};\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> Route = createRootRouteWithContext&lt;MyRouterContext&gt;()({\n  component: Root,\n});<\/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>This allows us access to the\u00a0<code>queryClient<\/code>\u00a0inside of our loader functions via the Router\u2019s context. If you&#8217;re wondering why we need loaders at all, now that we&#8217;re using react-query, stay tuned.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"querying\">Querying<\/h2>\n\n\n\n<p>We used Router&#8217;s built-in caching capabilities for our tasks. For epics, let&#8217;s use react-query. Moreover, let&#8217;s use the\u00a0<code>useSuspenseQuery<\/code>\u00a0hook, since managing loading state via Suspense boundaries is extremely ergonomic. Moreover, Suspense boundaries is exactly how Router&#8217;s\u00a0<code>pendingComponent<\/code>\u00a0works. So you can use\u00a0<code>useSuspenseQuery<\/code>, along with the same pendingComponent we looked at before!<\/p>\n\n\n\n<p>Let&#8217;s add another (contrived) summary query in our epics layout (route) component.<\/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\">export<\/span> <span class=\"hljs-keyword\">const<\/span> Route = createFileRoute(<span class=\"hljs-string\">\"\/app\/epics\"<\/span>)({\n  component: EpicLayout,\n  pendingComponent: <span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> &lt;div&gt;Loading epics route ...&lt;<span class=\"hljs-regexp\">\/div&gt;,\n});\n\nfunction EpicLayout() {\n  const context = Route.useRouteContext();\n  const { data } = useSuspenseQuery(epicsSummaryQueryOptions(context.timestarted));\n\n  return (\n    &lt;div&gt;\n      &lt;h2&gt;Epics overview&lt;\/<\/span>h2&gt;\n      &lt;div&gt;\n        {data.epicsOverview.map(<span class=\"hljs-function\"><span class=\"hljs-params\">epic<\/span> =&gt;<\/span> (\n          &lt;Fragment key={epic.name}&gt;\n            &lt;div&gt;{epic.name}&lt;<span class=\"hljs-regexp\">\/div&gt;\n            &lt;div&gt;{epic.count}&lt;\/<\/span>div&gt;\n          &lt;<span class=\"hljs-regexp\">\/Fragment&gt;\n        ))}\n      &lt;\/<\/span>div&gt;\n\n      &lt;div&gt;\n        &lt;Outlet \/&gt;\n      &lt;<span class=\"hljs-regexp\">\/div&gt;\n    &lt;\/<\/span>div&gt;\n  );\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>To keep the code somewhat organized (and other reasons we&#8217;ll get to) I stuck the query options into a separate place.<\/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\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> epicsSummaryQueryOptions = <span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">timestarted<\/span>: <span class=\"hljs-params\">number<\/span><\/span>) =&gt;<\/span> ({\n  queryKey: &#91;<span class=\"hljs-string\">\"epics\"<\/span>, <span class=\"hljs-string\">\"summary\"<\/span>],\n  queryFn: <span class=\"hljs-keyword\">async<\/span> () =&gt; {\n    <span class=\"hljs-keyword\">const<\/span> timeDifference = +<span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Date<\/span>() - timestarted;\n    <span class=\"hljs-built_in\">console<\/span>.log(<span class=\"hljs-string\">\"Running api\/epics\/overview query at\"<\/span>, timeDifference);\n    <span class=\"hljs-keyword\">const<\/span> epicsOverview = <span class=\"hljs-keyword\">await<\/span> fetchJson&lt;EpicOverview&#91;]&gt;(<span class=\"hljs-string\">\"api\/epics\/overview\"<\/span>);\n    <span class=\"hljs-keyword\">return<\/span> { epicsOverview };\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});<\/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>A query key, and function, and some cache settings. I&#8217;m passing in the timestarted value from context, so we can see when these queries fire. This will help us detect waterfalls.<\/p>\n\n\n\n<p>Let\u2019s look at the root epics page (with a few details removed for space).<\/p>\n\n\n<pre class=\"wp-block-code tsx-block\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"plaintext\" data-shcb-language-slug=\"plaintext\"><span><code class=\"hljs language-plaintext\">type SearchParams = {\n  page: number;\n};\n\nexport const Route = createFileRoute(\"\/app\/epics\/\")({\n  validateSearch(search: Record&lt;string, unknown&gt;): SearchParams {\n    return {\n      page: parseInt(search.page as string, 10) || 1,\n    };\n  },\n  loaderDeps: ({ search }) =&gt; {\n    return { page: search.page };\n  },\n  component: Index,\n  pendingComponent: () =&gt; &lt;div&gt;Loading epics ...&lt;\/div&gt;,\n  pendingMinMs: 3000,\n  pendingMs: 10,\n});\n\nfunction Index() {\n  const context = Route.useRouteContext();\n  const { page } = Route.useSearch();\n\n  const { data: epicsData } = useSuspenseQuery(epicsQueryOptions(context.timestarted, page));\n  const { data: epicsCount } = useSuspenseQuery(epicsCountQueryOptions(context.timestarted));\n\n  return (\n    &lt;div className=\"p-3\"&gt;\n      &lt;h3&gt;Epics page!&lt;\/h3&gt;\n      &lt;h3&gt;There are {epicsCount.count} epics&lt;\/h3&gt;\n      &lt;div&gt;\n        {epicsData.map((e, idx) =&gt; (\n          &lt;Fragment key={idx}&gt;\n            &lt;div&gt;{e.name}&lt;\/div&gt;\n          &lt;\/Fragment&gt;\n        ))}\n        &lt;div className=\"flex gap-3\"&gt;\n          &lt;Link to=\"\/app\/epics\" search={{ page: page - 1 }} disabled={page === 1}&gt;\n            Prev\n          &lt;\/Link&gt;\n          &lt;Link to=\"\/app\/epics\" search={{ page: page + 1 }} disabled={!epicsData.length}&gt;\n            Next\n          &lt;\/Link&gt;\n        &lt;\/div&gt;\n      &lt;\/div&gt;\n    &lt;\/div&gt;\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\">plaintext<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">plaintext<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<script>\ndocument.querySelector(\".tsx-block .shcb-language__name\").innerText = \"TypeScript\";\n<\/script>\n\n\n\n<p>Two queries on this page: one to get the list of (paged) epics, another to get the total count of all the epics. Let&#8217;s run it<\/p>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"779\" height=\"1024\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/11\/img-2-epics-rendered.jpg?resize=779%2C1024&#038;ssl=1\" alt=\"\" class=\"wp-image-4471\" style=\"width:363px;height:auto\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/11\/img-2-epics-rendered.jpg?resize=779%2C1024&amp;ssl=1 779w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/11\/img-2-epics-rendered.jpg?resize=228%2C300&amp;ssl=1 228w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/11\/img-2-epics-rendered.jpg?resize=768%2C1010&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/11\/img-2-epics-rendered.jpg?w=852&amp;ssl=1 852w\" sizes=\"auto, (max-width: 779px) 100vw, 779px\" \/><\/figure>\n\n\n\n<p>It&#8217;s as silly as before, but it does show the three pieces of data we&#8217;ve fetched: the overview data we fetched in the epics layout; and then the count of epics, and the list of epics we loaded in the epics page beneath that.<\/p>\n\n\n\n<p>What&#8217;s more, when we run this, we first see the pending component for our root route. That resolves quickly, and shows the main navigation, along with the pending component for our epics route. That resolves, showing the epics overview data, and then revealing the pending component for our epics page, which eventually resolves and shows the list and count of our epics.<\/p>\n\n\n\n<p>Our component-level data fetching is working, and integrating, via Suspense, with the same Router pending components we already had. Very cool!<\/p>\n\n\n\n<p>Let&#8217;s take a peak at our console though, and look at all the various logging we&#8217;ve been doing, to track when these fetches happen<\/p>\n\n\n\n<figure class=\"wp-block-image size-full is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"554\" height=\"248\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/11\/img-3-epics-waterfall.jpg?resize=554%2C248&#038;ssl=1\" alt=\"\" class=\"wp-image-4472\" style=\"width:386px;height:auto\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/11\/img-3-epics-waterfall.jpg?w=554&amp;ssl=1 554w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/11\/img-3-epics-waterfall.jpg?resize=300%2C134&amp;ssl=1 300w\" sizes=\"auto, (max-width: 554px) 100vw, 554px\" \/><\/figure>\n\n\n\n<p>The results are&#8230; awful. Component-level data fetching with Suspense feels really good, but if you&#8217;re not careful, these waterfalls are extremely easy to create. The problem is, when a component suspends while waiting for data, it prevents its children from rendering. This is precisely what&#8217;s happening here. The route is suspending, and not even giving the child component, which includes the page (and any other nested route components underneath) from rendering, which prevents those components&#8217; fetches from starting.<\/p>\n\n\n\n<p>There&#8217;s two potential solutions here: we could dump Suspense, and use the&nbsp;<code>useQuery<\/code>&nbsp;hook, instead, which does not suspend. That would require us to manually track multiple <code>isLoading<\/code> states (for each useQuery hook), and coordinate loading UX to go with that. For the epics page, we&#8217;d need to track both the count loading state, and the epics list state, and not show our UI until both have returned. And so on, for every other page.<\/p>\n\n\n\n<p>The other solution is to start pre-fetching these queries sooner.<\/p>\n\n\n\n<p>We&#8217;ll go with option 2.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"prefetching\">Prefetching<\/h3>\n\n\n\n<p>Remember previously we saw that loader functions all run in parallel. This is the perfect opportunity to start these queries off ahead of time, before the components even render. TanStack Query gives us an API to do just that.<\/p>\n\n\n\n<p>To prefetch with Query, we take the&nbsp;<code>queryClient<\/code>&nbsp;object we saw before, and call&nbsp;<code>queryClient.prefetchQuery<\/code>&nbsp;and pass in&nbsp;<strong>the exact same query options<\/strong>&nbsp;and Query will be smart enough, when the component loads and executes&nbsp;<code>useSuspenseQuery<\/code>, to see that the query is already in flight, and just latch onto that same request. That&#8217;s also a big reason why we put those query options into the&nbsp;<code>epicsSummaryQueryOptions<\/code>&nbsp;helper function: to make it easier to reuse in the loader, to prefetch.<\/p>\n\n\n\n<p>Here&#8217;s the loader we&#8217;ll add to the epics route:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">loader({ context }) {\n  <span class=\"hljs-keyword\">const<\/span> queryClient = context.queryClient;\n  queryClient.prefetchQuery(epicsSummaryQueryOptions(context.timestarted));\n},<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The loader receives the route tree&#8217;s context, from which it grabs the <code>queryClient<\/code>. From there, we call&nbsp;<code>prefetchQuery<\/code>&nbsp;and pass in the same options.<\/p>\n\n\n\n<p>Let&#8217;s move on to the Epics page. To review, this is the relevant code from our Epics page:<\/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-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">Index<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> context = Route.useRouteContext();\n  <span class=\"hljs-keyword\">const<\/span> { page } = Route.useSearch();\n\n  <span class=\"hljs-keyword\">const<\/span> { data: epicsData } = useSuspenseQuery(epicsQueryOptions(context.timestarted, page));\n  <span class=\"hljs-keyword\">const<\/span> { data: epicsCount } = useSuspenseQuery(epicsCountQueryOptions(context.timestarted));\n  \n  <span class=\"hljs-comment\">\/\/ ..<\/span><\/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<p>We grab the current page from the URL,\u00a0and\u00a0the context, for the timestarted value. Now let\u2019s do the same thing we just did, and repeat this code in the loader, to prefetch.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">async<\/span> loader({ context, deps }) {\n  <span class=\"hljs-keyword\">const<\/span> queryClient = context.queryClient;\n\n  queryClient.prefetchQuery(epicsQueryOptions(context.timestarted, deps.page));\n  queryClient.prefetchQuery(epicsCountQueryOptions(context.timestarted));\n},<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Now when we check the console, we see something a lot nicer.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"548\" height=\"254\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/11\/img-4-waterfall-solved.jpg?resize=548%2C254&#038;ssl=1\" alt=\"\" class=\"wp-image-4473\" style=\"width:362px;height:auto\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/11\/img-4-waterfall-solved.jpg?w=548&amp;ssl=1 548w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/11\/img-4-waterfall-solved.jpg?resize=300%2C139&amp;ssl=1 300w\" sizes=\"auto, (max-width: 548px) 100vw, 548px\" \/><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"fetching-state\">Fetching state<\/h3>\n\n\n\n<p>What happens when we <em>page up<\/em>. The page value will change in the URL, Router will send a new page value down into our loader, and our component. Then, our&nbsp;<code>useSuspenseQuery<\/code>&nbsp;will execute with new query values, and suspend again. That means our existing list of tasks will disappear, and show the &#8220;loading tasks&#8221; pending component. That would be a terrible UX.<\/p>\n\n\n\n<p>Fortunately, React offers us a nice solution, with the&nbsp;<code>useDeferredValue<\/code>&nbsp;hook. The docs are&nbsp;<a href=\"https:\/\/react.dev\/reference\/react\/useDeferredValue\">here<\/a>. This allows us to &#8220;defer&#8221; a state change. If a state change causes our deferred value on the page to suspend, React will keep the existing UI in place, and the deferred value will simply hold the old value. Let&#8217;s see it in action.<\/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-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">Index<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> { page } = Route.useSearch();\n  <span class=\"hljs-keyword\">const<\/span> context = Route.useRouteContext();\n\n  <span class=\"hljs-keyword\">const<\/span> deferredPage = useDeferredValue(page);\n  <span class=\"hljs-keyword\">const<\/span> loading = page !== deferredPage;\n\n  <span class=\"hljs-keyword\">const<\/span> { data: epicsData } = useSuspenseQuery(\n    epicsQueryOptions(context.timestarted, deferredPage)\n  );\n  <span class=\"hljs-keyword\">const<\/span> { data: epicsCount } = useSuspenseQuery(\n    epicsCountQueryOptions(context.timestarted)\n  );\n \n  <span class=\"hljs-comment\">\/\/ ...<\/span><\/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<p>We wrap the changing page value in&nbsp;<code>useDeferredValue<\/code>, and just like that, our page does not suspend when the new query is in flight. And to detect that a new query is running, we compare the real, correct&nbsp;<code>page<\/code>&nbsp;value, with the&nbsp;<code>deferredPage<\/code>&nbsp;value. If they&#8217;re different, we know new data are loading, and we can display a loading spinner (or in this case, put an opacity overlay on the epics list)<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"queries-are-re-used\">Queries are re-used!<\/h3>\n\n\n\n<p>When using react-query for data management, we can now re-use the same query across different routes. Both the view epic and edit epic pages need to fetch info on the epic the user is about to view, or edit. Now we can define those options in one place, like we had before.<\/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-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> epicQueryOptions = <span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">timestarted<\/span>: <span class=\"hljs-params\">number<\/span>, <span class=\"hljs-params\">id<\/span>: <span class=\"hljs-params\">string<\/span><\/span>) =&gt;<\/span> ({\n  queryKey: &#91;<span class=\"hljs-string\">\"epic\"<\/span>, id],\n  queryFn: <span class=\"hljs-keyword\">async<\/span> () =&gt; {\n    <span class=\"hljs-keyword\">const<\/span> timeDifference = +<span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Date<\/span>() - timestarted;\n\n    <span class=\"hljs-built_in\">console<\/span>.log(<span class=\"hljs-string\">`Loading api\/epic\/<span class=\"hljs-subst\">${id}<\/span> data at`<\/span>, timeDifference);\n    <span class=\"hljs-keyword\">const<\/span> epic = <span class=\"hljs-keyword\">await<\/span> fetchJson&lt;Epic&gt;(<span class=\"hljs-string\">`api\/epics\/<span class=\"hljs-subst\">${id}<\/span>`<\/span>);\n    <span class=\"hljs-keyword\">return<\/span> epic;\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});<\/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>We can use them in both routes, and have them be cached in between (assuming we set the caching values to allow that). You can try it <a href=\"https:\/\/github.com\/arackaf\/tanstack-router-loader-demo\">in the demo app<\/a>: view an epic, go back to the list, then edit the same epic (or vice versa). Only the first of those pages you visit should cause the fetch to happen in your network tab.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"updating-with-react-query\">Updating with react-query<\/h3>\n\n\n\n<p>Just like with tasks, epics have a page where we can edit an individual epic. Let&#8217;s see what the saving logic looks like with react-query.<\/p>\n\n\n\n<p>Let&#8217;s quickly review the query&nbsp;<em>keys<\/em>&nbsp;for the epics queries we&#8217;ve seen so far. For an individual epic, it was:<\/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\">export<\/span> <span class=\"hljs-keyword\">const<\/span> epicQueryOptions = <span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">timestarted<\/span>: <span class=\"hljs-params\">number<\/span>, <span class=\"hljs-params\">id<\/span>: <span class=\"hljs-params\">string<\/span><\/span>) =&gt;<\/span> ({\n  queryKey: &#91;<span class=\"hljs-string\">\"epic\"<\/span>, id],<\/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>For the epics list, it was this:<\/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> epicsQueryOptions = <span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">timestarted<\/span>: <span class=\"hljs-params\">number<\/span>, <span class=\"hljs-params\">page<\/span>: <span class=\"hljs-params\">number<\/span><\/span>) =&gt;<\/span> ({\n  queryKey: &#91;<span class=\"hljs-string\">\"epics\"<\/span>, <span class=\"hljs-string\">\"list\"<\/span>, page],<\/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>And the count:<\/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\">export<\/span> <span class=\"hljs-keyword\">const<\/span> epicsCountQueryOptions = <span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">timestarted<\/span>: <span class=\"hljs-params\">number<\/span><\/span>) =&gt;<\/span> ({\n  queryKey: &#91;<span class=\"hljs-string\">\"epics\"<\/span>, <span class=\"hljs-string\">\"count\"<\/span>],<\/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>Finally, the epics overview:<\/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\">export<\/span> <span class=\"hljs-keyword\">const<\/span> epicsSummaryQueryOptions = <span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">timestarted<\/span>: <span class=\"hljs-params\">number<\/span><\/span>) =&gt;<\/span> ({\n  queryKey: &#91;<span class=\"hljs-string\">\"epics\"<\/span>, <span class=\"hljs-string\">\"summary\"<\/span>],<\/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>Notice the pattern:&nbsp;<code>epics<\/code>&nbsp;followed by various things for the queries that affected multiple epics, and for an individual epic, we did&nbsp;<code>['epic', ${epicId}]<\/code>. With that in mind, let&#8217;s see just how easy it is to invalidate these queries after a mutation:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-17\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-code-table\"><span class='shcb-loc'><span><span class=\"hljs-keyword\">const<\/span> save = <span class=\"hljs-keyword\">async<\/span> () =&gt; {\n<\/span><\/span><span class='shcb-loc'><span>  setSaving(<span class=\"hljs-literal\">true<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">await<\/span> postToApi(<span class=\"hljs-string\">\"api\/epic\/update\"<\/span>, {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attr\">id<\/span>: epic.id,\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attr\">name<\/span>: newName.current!.value,\n<\/span><\/span><span class='shcb-loc'><span>  });\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><mark class='shcb-loc'><span>  queryClient.removeQueries({ <span class=\"hljs-attr\">queryKey<\/span>: &#91;<span class=\"hljs-string\">\"epics\"<\/span>] });\n<\/span><\/mark><mark class='shcb-loc'><span>  queryClient.removeQueries({ <span class=\"hljs-attr\">queryKey<\/span>: &#91;<span class=\"hljs-string\">\"epic\"<\/span>, epicId] });\n<\/span><\/mark><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  navigate({ <span class=\"hljs-attr\">to<\/span>: <span class=\"hljs-string\">\"\/app\/epics\"<\/span>, <span class=\"hljs-attr\">search<\/span>: { <span class=\"hljs-attr\">page<\/span>: <span class=\"hljs-number\">1<\/span> } });\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  setSaving(<span class=\"hljs-literal\">false<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>};\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-17\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The magic is on the highlighted lines.<\/p>\n\n\n\n<p>With one fell sweep, we remove&nbsp;<strong>all<\/strong>&nbsp;cached entries for&nbsp;<strong>any<\/strong>&nbsp;query that&nbsp;<em>started with<\/em>&nbsp;<code>epics<\/code>, or started with&nbsp;<code>['epic', ${epicId}]<\/code>, and Query will handle the rest. Now, when we navigate back to the epics page (or any page that used these queries), we&#8217;ll see the suspense boundary show, while fresh data are loaded. If you&#8217;d prefer to keep stale data on the screen, while the fresh data load, that&#8217;s fine too: just use&nbsp;<code>queryClient.invalidateQueries<\/code>&nbsp;instead. If you&#8217;d like to detect if a query is re-fetching in the background, so you can display an inline spinner, use the&nbsp;<code>isFetching<\/code>&nbsp;property returned from&nbsp;<code>useSuspenseQuery<\/code>.<\/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> { data: epicsData, isFetching } = useSuspenseQuery(\n  epicsQueryOptions(context.timestarted, deferredPage)\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<h3 class=\"wp-block-heading\" id=\"odds-and-ends\">Odds and ends<\/h3>\n\n\n\n<p>We&#8217;ve gone pretty deep on TanStack Route and Query. Let&#8217;s take a look at one last trick. <\/p>\n\n\n\n<p>If you recall, we saw that pending components ship a related\u00a0<code>pendingMinMs<\/code>\u00a0that forced a pending component to stay on the page a minimum amount of time, even if the data were ready. This was to avoid a jarring flash of a loading state. We also saw that TanStack Router uses Suspense to show those pending components, which means that react-query&#8217;s\u00a0<code>useSuspenseQuery<\/code>\u00a0will seamlessly integrate with it. Well, almost seamlessly. Router can only use the\u00a0<code>pendingMinMs<\/code>\u00a0value with the promise we return from the Router\u2019s loader. But now we don&#8217;t really return any promise from the loader; we prefetch some stuff, and rely on component-level data fetching to do the real work.<\/p>\n\n\n\n<p>Well there&#8217;s nothing stopping you from doing both! Right now our loader looks like this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-19\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">async<\/span> loader({ context, deps }) {\n  <span class=\"hljs-keyword\">const<\/span> queryClient = context.queryClient;\n\n  queryClient.prefetchQuery(epicsQueryOptions(context.timestarted, deps.page));\n  queryClient.prefetchQuery(epicsCountQueryOptions(context.timestarted));\n},<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-19\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Query also ships with a&nbsp;<code>queryClient.ensureQueryData<\/code>&nbsp;method, which can load query data, and return a promise for that request. Let&#8217;s put it to good use so we can use&nbsp;<code>pendingMinMs<\/code>&nbsp;again.<\/p>\n\n\n\n<p>One thing you do&nbsp;<em>not<\/em>&nbsp;want to do is this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-20\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">await<\/span> queryClient.ensureQueryData(epicsQueryOptions(context.timestarted, deps.page)),\n<span class=\"hljs-keyword\">await<\/span> queryClient.ensureQueryData(epicsCountQueryOptions(context.timestarted)),<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-20\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>That will block on each request, serially. In other words, a waterfall. Instead, to kick off both requests immediately and wait on them in the loader (without a waterfall), you can do this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-21\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">await<\/span> <span class=\"hljs-built_in\">Promise<\/span>.allSettled(&#91;\n  queryClient.ensureQueryData(epicsQueryOptions(context.timestarted, deps.page)),\n  queryClient.ensureQueryData(epicsCountQueryOptions(context.timestarted)),\n]);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-21\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Which works, and keeps the pending component on the screen for the duration of&nbsp;<code>pendingMinMs<\/code><\/p>\n\n\n\n<p>You won&#8217;t always, or even usually need to do this. But it&#8217;s handy for when you do.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"wrapping-up\">Wrapping up<\/h2>\n\n\n\n<p>This has been a whirlwind route of TanStack Router and TanStack Query, but hopefully not an overwhelming one. These tools are incredibly powerful, and offer the ability to do just about anything. I hope this post will help some people put them to good use!<\/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\/introducing-tanstack-router\/\">Introducing TanStack Router<\/a>\n            <\/li>\n                      <li>\n              <a href=\"https:\/\/frontendmasters.com\/blog\/tanstack-router-data-loading-1\/\">Loading Data with TanStack Router: Getting Going<\/a>\n            <\/li>\n                      <li>\n              <a href=\"https:\/\/frontendmasters.com\/blog\/tanstack-router-data-loading-2\/\">Loading Data with TanStack Router: react-query<\/a>\n            <\/li>\n                  <\/ol>\n        <\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>TanStack Query, or react-query, simplifies client-side data fetching with features like caching, automatic re-fetching, and error handling. It integrates smoothly with TanStack Router, allowing efficient prefetching and loading states using hooks like useSuspenseQuery. <\/p>\n","protected":false},"author":21,"featured_media":1934,"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-4492","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\/2024\/04\/tanstack.jpg?fit=1000%2C500&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/4492","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=4492"}],"version-history":[{"count":21,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/4492\/revisions"}],"predecessor-version":[{"id":4567,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/4492\/revisions\/4567"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/1934"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=4492"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=4492"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=4492"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}