this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fuckkkkkkkkkkkkkkkkkk

+426 -47
+56
src/components/console/query-trace-utils.ts
··· 1 + import type { QueryStatus } from "@tanstack/react-query"; 2 + 3 + type FetchStatus = "fetching" | "idle" | "paused"; 4 + 5 + export function getCacheStatus(dataUpdatedAt?: number) { 6 + return dataUpdatedAt 7 + ? { indicator: "cached" as const, label: "cache populated" } 8 + : { indicator: "idle" as const, label: "cache empty" }; 9 + } 10 + 11 + export function getFetchStatus(fetchStatus?: FetchStatus, queryStatus?: QueryStatus) { 12 + if (queryStatus === "error") { 13 + return { indicator: "error" as const, label: "error" }; 14 + } 15 + 16 + if (fetchStatus === "fetching") { 17 + return { indicator: "fetching" as const, label: "fetching" }; 18 + } 19 + 20 + if (fetchStatus === "paused") { 21 + return { indicator: "idle" as const, label: "paused" }; 22 + } 23 + 24 + return { indicator: "idle" as const, label: "request idle" }; 25 + } 26 + 27 + export function getLoadingCacheStatus() { 28 + return { indicator: "idle" as const, label: "cache empty" }; 29 + } 30 + 31 + export function getLoadingFetchStatus() { 32 + return { indicator: "fetching" as const, label: "fetching" }; 33 + } 34 + 35 + export function getPreloadStatus(dataUpdatedAt?: number) { 36 + return dataUpdatedAt 37 + ? { indicator: "cached" as const, label: "preload complete" } 38 + : { indicator: "idle" as const, label: "not preloaded" }; 39 + } 40 + 41 + export function getLoadingPreloadStatus() { 42 + return { indicator: "fetching" as const, label: "preloading" }; 43 + } 44 + 45 + export function formatPokemonListQueryKey(location: string, offset: number) { 46 + return `pokemon-list / ${location} / offset ${offset}`; 47 + } 48 + 49 + export function formatFilteredPokemonListQueryKey( 50 + location: string, 51 + offset: number, 52 + nameFilter: string, 53 + ) { 54 + const filter = nameFilter ? `name "${nameFilter}"` : 'name ""'; 55 + return `pokemon-list / ${location} / offset ${offset} / ${filter}`; 56 + }
+77
src/components/console/query-trace.tsx
··· 1 + import type { ReactNode } from "react"; 2 + import { StatusDotWithLabel } from "./status-dot"; 3 + 4 + type QueryTraceIndicator = "cached" | "fetching" | "idle" | "error"; 5 + 6 + interface QueryTraceStatus { 7 + indicator: QueryTraceIndicator; 8 + label: string; 9 + } 10 + 11 + interface QueryTraceProps { 12 + behaviorDescription: ReactNode; 13 + cacheStatus: QueryTraceStatus; 14 + fetchStatus: QueryTraceStatus; 15 + preloadStatus?: QueryTraceStatus; 16 + queryKeys: ReactNode[]; 17 + strategyDescription: ReactNode; 18 + } 19 + 20 + export function QueryTrace({ 21 + behaviorDescription, 22 + cacheStatus, 23 + fetchStatus, 24 + preloadStatus, 25 + queryKeys, 26 + strategyDescription, 27 + }: QueryTraceProps) { 28 + return ( 29 + <section 30 + className="mb-5 border border-(--border-default) bg-(--bg-secondary) p-4 font-mono" 31 + aria-label="Query behavior" 32 + > 33 + <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between"> 34 + <div className="max-w-2xl"> 35 + <p className="text-xs font-semibold uppercase tracking-wider text-(--text-secondary)"> 36 + Behavior 37 + </p> 38 + <p className="mt-1 text-sm leading-relaxed text-(--text-primary)"> 39 + {behaviorDescription} 40 + </p> 41 + </div> 42 + <div className="flex flex-wrap gap-3 text-xs"> 43 + <StatusDotWithLabel status={cacheStatus.indicator} label={cacheStatus.label} /> 44 + {preloadStatus && ( 45 + <StatusDotWithLabel status={preloadStatus.indicator} label={preloadStatus.label} /> 46 + )} 47 + <StatusDotWithLabel status={fetchStatus.indicator} label={fetchStatus.label} /> 48 + </div> 49 + </div> 50 + 51 + <dl className="mt-4 grid gap-3 border-t border-(--border-default) pt-3 text-xs sm:grid-cols-4"> 52 + <div> 53 + <dt className="mb-1 uppercase tracking-wider text-(--text-muted)">Strategy</dt> 54 + <dd className="text-(--text-primary)">{strategyDescription}</dd> 55 + </div> 56 + <div> 57 + <dt className="mb-1 uppercase tracking-wider text-(--text-muted)">Query keys</dt> 58 + <dd className="space-y-1 text-(--text-primary)"> 59 + {queryKeys.map((queryKey, index) => ( 60 + <div key={index}>{queryKey}</div> 61 + ))} 62 + </dd> 63 + </div> 64 + <div> 65 + <dt className="mb-1 uppercase tracking-wider text-(--text-muted)">Fetch status</dt> 66 + <dd className="text-(--text-primary)">{fetchStatus.label}</dd> 67 + </div> 68 + {preloadStatus && ( 69 + <div> 70 + <dt className="mb-1 uppercase tracking-wider text-(--text-muted)">Preload status</dt> 71 + <dd className="text-(--text-primary)">{preloadStatus.label}</dd> 72 + </div> 73 + )} 74 + </dl> 75 + </section> 76 + ); 77 + }
+2 -2
src/components/pagination-nav.tsx
··· 3 3 4 4 interface PaginationNavProps { 5 5 prefetch?: "intent" | "viewport" | "render" | false; 6 - prevOffset: number | undefined; 7 - nextOffset: number | undefined; 6 + prevOffset: number | null; 7 + nextOffset: number | null; 8 8 to: keyof FileRoutesByPath; 9 9 } 10 10
+43 -7
src/routes/basic.tsx
··· 2 2 import { createFileRoute } from "@tanstack/react-router"; 3 3 import { useQuery, useSuspenseQuery } from "@tanstack/react-query"; 4 4 import * as v from "valibot"; 5 + import { QueryTrace } from "~/components/console/query-trace"; 6 + import { 7 + formatPokemonListQueryKey, 8 + getCacheStatus, 9 + getFetchStatus, 10 + getLoadingCacheStatus, 11 + getLoadingFetchStatus, 12 + } from "~/components/console/query-trace-utils"; 5 13 import { PaginationNav } from "~/components/pagination-nav"; 6 14 import { ConsoleCard } from "~/components/console/console-card"; 7 15 import { SectionHeader } from "~/components/console/section-header"; ··· 16 24 offset: v.optional(v.number(), 0), 17 25 }); 18 26 27 + const getBasicQueryTraceProps = (currentOffset: number) => ({ 28 + behaviorDescription: 29 + "No preload: the Pokémon query starts after this route renders. Until it resolves, the table shows its loading state.", 30 + queryKeys: [formatPokemonListQueryKey("suspense", currentOffset)], 31 + strategyDescription: "none", 32 + }); 33 + 19 34 export const Route = createFileRoute("/basic")({ 20 35 validateSearch: searchParamsSchema, 21 36 component: RouteComponent, ··· 34 49 National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 35 50 </h1> 36 51 <div className="min-h-[500px]"> 37 - <Suspense fallback={<PokemonTableSkeleton rowCount={POKEMON_LIMIT} />}> 52 + <Suspense 53 + fallback={ 54 + <> 55 + <QueryTrace 56 + {...getBasicQueryTraceProps(currentOffset)} 57 + cacheStatus={getLoadingCacheStatus()} 58 + fetchStatus={getLoadingFetchStatus()} 59 + /> 60 + <PokemonTableSkeleton rowCount={POKEMON_LIMIT} /> 61 + </> 62 + } 63 + > 38 64 <PokemonTableContent currentOffset={currentOffset} /> 39 65 </Suspense> 40 66 </div> ··· 46 72 } 47 73 48 74 function PokemonTableContent({ currentOffset }: { currentOffset: number }) { 49 - const { data } = useSuspenseQuery({ 50 - queryKey: getPokemonListQueryKey("suspense", currentOffset), 75 + const queryKey = getPokemonListQueryKey("suspense", currentOffset); 76 + const { data, dataUpdatedAt, fetchStatus, isFetching } = useSuspenseQuery({ 77 + queryKey, 51 78 queryFn: getPokemonListQueryFn, 52 79 }); 53 80 54 - return <PokemonTable pokemon={data.pokemon} />; 81 + return ( 82 + <> 83 + <QueryTrace 84 + {...getBasicQueryTraceProps(currentOffset)} 85 + cacheStatus={getCacheStatus(dataUpdatedAt)} 86 + fetchStatus={getFetchStatus(fetchStatus, isFetching ? "pending" : "success")} 87 + /> 88 + <PokemonTable pokemon={data.pokemon} /> 89 + </> 90 + ); 55 91 } 56 92 57 93 function PaginationNavOutlet() { 58 94 const { offset: currentOffset } = Route.useSearch(); 59 - const { data, isPending } = useQuery({ 95 + const { data } = useQuery({ 60 96 queryKey: getPokemonListQueryKey("suspense", currentOffset), 61 97 queryFn: getPokemonListQueryFn, 62 98 }); 63 99 64 100 return ( 65 101 <PaginationNav 66 - prevOffset={isPending ? undefined : (data?.prevOffset ?? undefined)} 67 - nextOffset={isPending ? undefined : (data?.nextOffset ?? undefined)} 102 + prevOffset={data?.prevOffset ?? null} 103 + nextOffset={data?.nextOffset ?? null} 68 104 to="/basic" 69 105 /> 70 106 );
+50 -7
src/routes/debounced-preload-filters.tsx
··· 3 3 import { useDebouncedCallback } from "@tanstack/react-pacer"; 4 4 import { queryOptions, useQuery, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; 5 5 import * as v from "valibot"; 6 + import { QueryTrace } from "~/components/console/query-trace"; 7 + import { 8 + formatFilteredPokemonListQueryKey, 9 + getCacheStatus, 10 + getFetchStatus, 11 + getLoadingCacheStatus, 12 + getLoadingFetchStatus, 13 + getLoadingPreloadStatus, 14 + getPreloadStatus, 15 + } from "~/components/console/query-trace-utils"; 6 16 import { FilterForm, FilterSubmitContext } from "~/components/filter-form"; 7 17 import { PaginationNav } from "~/components/pagination-nav"; 8 18 import { ConsoleCard } from "~/components/console/console-card"; ··· 17 27 const searchParamsSchema = v.object({ 18 28 offset: v.optional(v.number(), 0), 19 29 name: v.optional(v.string(), ""), 30 + }); 31 + 32 + const getDebouncedQueryTraceProps = (currentOffset: number, nameFilter: string) => ({ 33 + behaviorDescription: 34 + "Debounced preload: typing starts filtered Pokémon prefetches before submit, then the route loader confirms the submitted query.", 35 + queryKeys: [ 36 + formatFilteredPokemonListQueryKey("debounced-preload-filters", currentOffset, nameFilter), 37 + ], 38 + strategyDescription: "100ms debounced prefetchQuery + loader prefetchQuery", 20 39 }); 21 40 22 41 export const Route = createFileRoute("/debounced-preload-filters")({ ··· 129 148 </h1> 130 149 131 150 <div className="min-h-[500px]"> 132 - <Suspense fallback={<PokemonTableSkeleton rowCount={POKEMON_LIMIT} />}> 133 - <PokemonTableContent nameFilter={nameFilter} /> 151 + <Suspense 152 + fallback={ 153 + <> 154 + <QueryTrace 155 + {...getDebouncedQueryTraceProps(currentOffset, nameFilter)} 156 + cacheStatus={getLoadingCacheStatus()} 157 + fetchStatus={getLoadingFetchStatus()} 158 + preloadStatus={getLoadingPreloadStatus()} 159 + /> 160 + <PokemonTableSkeleton rowCount={POKEMON_LIMIT} /> 161 + </> 162 + } 163 + > 164 + <PokemonTableContent currentOffset={currentOffset} nameFilter={nameFilter} /> 134 165 </Suspense> 135 166 </div> 136 167 <PaginationNavOutlet /> ··· 140 171 ); 141 172 } 142 173 143 - function PokemonTableContent({ nameFilter }: { nameFilter: string }) { 174 + function PokemonTableContent({ 175 + currentOffset, 176 + nameFilter, 177 + }: { 178 + currentOffset: number; 179 + nameFilter: string; 180 + }) { 144 181 const { pokemonListOptions } = useRouteContext({ from: "/debounced-preload-filters" }); 145 - const { data } = useSuspenseQuery(pokemonListOptions); 182 + const { data, dataUpdatedAt, fetchStatus, status } = useSuspenseQuery(pokemonListOptions); 146 183 const filteredPokemon = data.pokemon; 147 184 148 185 return ( 149 186 <> 187 + <QueryTrace 188 + {...getDebouncedQueryTraceProps(currentOffset, nameFilter)} 189 + cacheStatus={getCacheStatus(dataUpdatedAt)} 190 + fetchStatus={getFetchStatus(fetchStatus, status)} 191 + preloadStatus={getPreloadStatus(dataUpdatedAt)} 192 + /> 150 193 <PokemonTable pokemon={filteredPokemon} /> 151 194 152 195 {filteredPokemon.length === 0 && nameFilter && ( ··· 160 203 161 204 function PaginationNavOutlet() { 162 205 const { pokemonListOptions } = useRouteContext({ from: "/debounced-preload-filters" }); 163 - const { data, isPending } = useQuery(pokemonListOptions); 206 + const { data } = useQuery(pokemonListOptions); 164 207 165 208 return ( 166 209 <PaginationNav 167 210 prefetch="viewport" 168 - prevOffset={isPending ? undefined : (data?.prevOffset ?? undefined)} 169 - nextOffset={isPending ? undefined : (data?.nextOffset ?? undefined)} 211 + prevOffset={data?.prevOffset ?? null} 212 + nextOffset={data?.nextOffset ?? null} 170 213 to="/debounced-preload-filters" 171 214 /> 172 215 );
+48 -7
src/routes/filters.tsx
··· 2 2 import { createFileRoute, useRouteContext } from "@tanstack/react-router"; 3 3 import { queryOptions, useQuery, useSuspenseQuery } from "@tanstack/react-query"; 4 4 import * as v from "valibot"; 5 + import { QueryTrace } from "~/components/console/query-trace"; 6 + import { 7 + formatFilteredPokemonListQueryKey, 8 + getCacheStatus, 9 + getFetchStatus, 10 + getLoadingCacheStatus, 11 + getLoadingFetchStatus, 12 + getLoadingPreloadStatus, 13 + getPreloadStatus, 14 + } from "~/components/console/query-trace-utils"; 5 15 import { FilterForm, FilterSubmitContext } from "~/components/filter-form"; 6 16 import { PaginationNav } from "~/components/pagination-nav"; 7 17 import { ConsoleCard } from "~/components/console/console-card"; ··· 16 26 const searchParamsSchema = v.object({ 17 27 offset: v.optional(v.number(), 0), 18 28 name: v.optional(v.string(), ""), 29 + }); 30 + 31 + const getFiltersQueryTraceProps = (currentOffset: number, nameFilter: string) => ({ 32 + behaviorDescription: 33 + "Submitted search preload: the route loader prefetches the filtered Pokémon query after the search params change.", 34 + queryKeys: [formatFilteredPokemonListQueryKey("filters", currentOffset, nameFilter)], 35 + strategyDescription: "filter submit updates search params + loader prefetchQuery", 19 36 }); 20 37 21 38 function FilterSubmitContextProvider(props: { ··· 98 115 </h1> 99 116 100 117 <div className="min-h-[500px]"> 101 - <Suspense fallback={<PokemonTableSkeleton rowCount={POKEMON_LIMIT} />}> 102 - <PokemonTableContent nameFilter={nameFilter} /> 118 + <Suspense 119 + fallback={ 120 + <> 121 + <QueryTrace 122 + {...getFiltersQueryTraceProps(currentOffset, nameFilter)} 123 + cacheStatus={getLoadingCacheStatus()} 124 + fetchStatus={getLoadingFetchStatus()} 125 + preloadStatus={getLoadingPreloadStatus()} 126 + /> 127 + <PokemonTableSkeleton rowCount={POKEMON_LIMIT} /> 128 + </> 129 + } 130 + > 131 + <PokemonTableContent currentOffset={currentOffset} nameFilter={nameFilter} /> 103 132 </Suspense> 104 133 </div> 105 134 <PaginationNavOutlet /> ··· 109 138 ); 110 139 } 111 140 112 - function PokemonTableContent({ nameFilter }: { nameFilter: string }) { 141 + function PokemonTableContent({ 142 + currentOffset, 143 + nameFilter, 144 + }: { 145 + currentOffset: number; 146 + nameFilter: string; 147 + }) { 113 148 const { pokemonListOptions } = useRouteContext({ from: "/filters" }); 114 - const { data } = useSuspenseQuery(pokemonListOptions); 149 + const { data, dataUpdatedAt, fetchStatus, status } = useSuspenseQuery(pokemonListOptions); 115 150 const filteredPokemon = data.pokemon; 116 151 117 152 return ( 118 153 <> 154 + <QueryTrace 155 + {...getFiltersQueryTraceProps(currentOffset, nameFilter)} 156 + cacheStatus={getCacheStatus(dataUpdatedAt)} 157 + fetchStatus={getFetchStatus(fetchStatus, status)} 158 + preloadStatus={getPreloadStatus(dataUpdatedAt)} 159 + /> 119 160 <PokemonTable pokemon={filteredPokemon} /> 120 161 121 162 {filteredPokemon.length === 0 && nameFilter && ( ··· 129 170 130 171 function PaginationNavOutlet() { 131 172 const { pokemonListOptions } = useRouteContext({ from: "/filters" }); 132 - const { data, isPending } = useQuery(pokemonListOptions); 173 + const { data } = useQuery(pokemonListOptions); 133 174 134 175 return ( 135 176 <PaginationNav 136 177 prefetch="viewport" 137 - prevOffset={isPending ? undefined : (data?.prevOffset ?? undefined)} 138 - nextOffset={isPending ? undefined : (data?.nextOffset ?? undefined)} 178 + prevOffset={data?.prevOffset ?? null} 179 + nextOffset={data?.nextOffset ?? null} 139 180 to="/filters" 140 181 /> 141 182 );
+47 -8
src/routes/intent-preloading.tsx
··· 2 2 import { createFileRoute, useRouteContext } from "@tanstack/react-router"; 3 3 import { queryOptions, useQuery, useSuspenseQuery } from "@tanstack/react-query"; 4 4 import * as v from "valibot"; 5 + import { QueryTrace } from "~/components/console/query-trace"; 6 + import { 7 + formatPokemonListQueryKey, 8 + getCacheStatus, 9 + getFetchStatus, 10 + getLoadingCacheStatus, 11 + getLoadingFetchStatus, 12 + getLoadingPreloadStatus, 13 + getPreloadStatus, 14 + } from "~/components/console/query-trace-utils"; 5 15 import { PaginationNav } from "~/components/pagination-nav"; 6 16 import { ConsoleCard } from "~/components/console/console-card"; 7 17 import { SectionHeader } from "~/components/console/section-header"; ··· 16 26 offset: v.optional(v.number(), 0), 17 27 }); 18 28 29 + const getIntentPreloadingQueryTraceProps = (currentOffset: number) => ({ 30 + behaviorDescription: 31 + "Intent preload: hovering a navigation target starts the Pokémon query before click, then the route loader ensures the active page is prefetched.", 32 + queryKeys: [formatPokemonListQueryKey("intent-preloading", currentOffset)], 33 + strategyDescription: 'link preload="intent" + loader prefetchQuery', 34 + }); 35 + 19 36 export const Route = createFileRoute("/intent-preloading")({ 20 37 validateSearch: searchParamsSchema, 21 38 loaderDeps: ({ search }) => ({ ··· 52 69 National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 53 70 </h1> 54 71 <div className="min-h-[500px]"> 55 - <Suspense fallback={<PokemonTableSkeleton rowCount={POKEMON_LIMIT} />}> 56 - <PokemonTableContent /> 72 + <Suspense 73 + fallback={ 74 + <> 75 + <QueryTrace 76 + {...getIntentPreloadingQueryTraceProps(currentOffset)} 77 + cacheStatus={getLoadingCacheStatus()} 78 + fetchStatus={getLoadingFetchStatus()} 79 + preloadStatus={getLoadingPreloadStatus()} 80 + /> 81 + <PokemonTableSkeleton rowCount={POKEMON_LIMIT} /> 82 + </> 83 + } 84 + > 85 + <PokemonTableContent currentOffset={currentOffset} /> 57 86 </Suspense> 58 87 </div> 59 88 <PaginationNavOutlet /> ··· 63 92 ); 64 93 } 65 94 66 - function PokemonTableContent() { 95 + function PokemonTableContent({ currentOffset }: { currentOffset: number }) { 67 96 const { pokemonListOptions } = useRouteContext({ from: "/intent-preloading" }); 68 - const { data } = useSuspenseQuery(pokemonListOptions); 97 + const { data, dataUpdatedAt, fetchStatus, status } = useSuspenseQuery(pokemonListOptions); 69 98 70 - return <PokemonTable pokemon={data.pokemon} />; 99 + return ( 100 + <> 101 + <QueryTrace 102 + {...getIntentPreloadingQueryTraceProps(currentOffset)} 103 + cacheStatus={getCacheStatus(dataUpdatedAt)} 104 + fetchStatus={getFetchStatus(fetchStatus, status)} 105 + preloadStatus={getPreloadStatus(dataUpdatedAt)} 106 + /> 107 + <PokemonTable pokemon={data.pokemon} /> 108 + </> 109 + ); 71 110 } 72 111 73 112 function PaginationNavOutlet() { 74 113 const { pokemonListOptions } = useRouteContext({ from: "/intent-preloading" }); 75 - const { data, isPending } = useQuery(pokemonListOptions); 114 + const { data } = useQuery(pokemonListOptions); 76 115 77 116 return ( 78 117 <PaginationNav 79 118 prefetch="intent" 80 - prevOffset={isPending ? undefined : (data?.prevOffset ?? undefined)} 81 - nextOffset={isPending ? undefined : (data?.nextOffset ?? undefined)} 119 + prevOffset={data?.prevOffset ?? null} 120 + nextOffset={data?.nextOffset ?? null} 82 121 to="/intent-preloading" 83 122 /> 84 123 );
+55 -8
src/routes/pagination.tsx
··· 2 2 import { createFileRoute, useRouteContext } from "@tanstack/react-router"; 3 3 import { queryOptions, useQuery, useSuspenseQuery } from "@tanstack/react-query"; 4 4 import * as v from "valibot"; 5 + import { QueryTrace } from "~/components/console/query-trace"; 6 + import { 7 + formatPokemonListQueryKey, 8 + getCacheStatus, 9 + getFetchStatus, 10 + getLoadingCacheStatus, 11 + getLoadingFetchStatus, 12 + getLoadingPreloadStatus, 13 + getPreloadStatus, 14 + } from "~/components/console/query-trace-utils"; 5 15 import { PaginationNav } from "~/components/pagination-nav"; 6 16 import { ConsoleCard } from "~/components/console/console-card"; 7 17 import { SectionHeader } from "~/components/console/section-header"; ··· 16 26 offset: v.optional(v.number(), 0), 17 27 }); 18 28 29 + const getPaginationQueryKeys = (currentOffset: number) => [ 30 + `current: ${formatPokemonListQueryKey("pagination", currentOffset)}`, 31 + ...(currentOffset > 0 32 + ? [`previous: ${formatPokemonListQueryKey("pagination", currentOffset - POKEMON_LIMIT)}`] 33 + : []), 34 + `next: ${formatPokemonListQueryKey("pagination", currentOffset + POKEMON_LIMIT)}`, 35 + ]; 36 + 37 + const getPaginationQueryTraceProps = (currentOffset: number) => ({ 38 + behaviorDescription: 39 + "Viewport preload: the current route is prefetched by the loader, and visible pagination links preload adjacent pages before click.", 40 + queryKeys: getPaginationQueryKeys(currentOffset), 41 + strategyDescription: 'loader prefetchQuery + pagination preload="viewport"', 42 + }); 43 + 19 44 export const Route = createFileRoute("/pagination")({ 20 45 validateSearch: searchParamsSchema, 21 46 loaderDeps: ({ search }) => ({ ··· 52 77 National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 53 78 </h1> 54 79 <div className="min-h-[500px]"> 55 - <Suspense fallback={<PokemonTableSkeleton rowCount={POKEMON_LIMIT} />}> 56 - <PokemonTableContent /> 80 + <Suspense 81 + fallback={ 82 + <> 83 + <QueryTrace 84 + {...getPaginationQueryTraceProps(currentOffset)} 85 + cacheStatus={getLoadingCacheStatus()} 86 + fetchStatus={getLoadingFetchStatus()} 87 + preloadStatus={getLoadingPreloadStatus()} 88 + /> 89 + <PokemonTableSkeleton rowCount={POKEMON_LIMIT} /> 90 + </> 91 + } 92 + > 93 + <PokemonTableContent currentOffset={currentOffset} /> 57 94 </Suspense> 58 95 </div> 59 96 <PaginationNavOutlet /> ··· 63 100 ); 64 101 } 65 102 66 - function PokemonTableContent() { 103 + function PokemonTableContent({ currentOffset }: { currentOffset: number }) { 67 104 const { pokemonListOptions } = useRouteContext({ from: "/pagination" }); 68 - const { data } = useSuspenseQuery(pokemonListOptions); 105 + const { data, dataUpdatedAt, fetchStatus, status } = useSuspenseQuery(pokemonListOptions); 69 106 70 - return <PokemonTable pokemon={data.pokemon} />; 107 + return ( 108 + <> 109 + <QueryTrace 110 + {...getPaginationQueryTraceProps(currentOffset)} 111 + cacheStatus={getCacheStatus(dataUpdatedAt)} 112 + fetchStatus={getFetchStatus(fetchStatus, status)} 113 + preloadStatus={getPreloadStatus(dataUpdatedAt)} 114 + /> 115 + <PokemonTable pokemon={data.pokemon} /> 116 + </> 117 + ); 71 118 } 72 119 73 120 function PaginationNavOutlet() { 74 121 const { pokemonListOptions } = useRouteContext({ from: "/pagination" }); 75 - const { data, isPending } = useQuery(pokemonListOptions); 122 + const { data } = useQuery(pokemonListOptions); 76 123 77 124 return ( 78 125 <PaginationNav 79 126 prefetch="viewport" 80 - prevOffset={isPending ? undefined : (data?.prevOffset ?? undefined)} 81 - nextOffset={isPending ? undefined : (data?.nextOffset ?? undefined)} 127 + prevOffset={data?.prevOffset ?? null} 128 + nextOffset={data?.nextOffset ?? null} 82 129 to="/pagination" 83 130 /> 84 131 );
+48 -8
src/routes/preloading.tsx
··· 2 2 import { createFileRoute, useRouteContext } from "@tanstack/react-router"; 3 3 import { queryOptions, useQuery, useSuspenseQuery } from "@tanstack/react-query"; 4 4 import * as v from "valibot"; 5 + import { QueryTrace } from "~/components/console/query-trace"; 6 + import { 7 + formatPokemonListQueryKey, 8 + getCacheStatus, 9 + getFetchStatus, 10 + getLoadingCacheStatus, 11 + getLoadingFetchStatus, 12 + getLoadingPreloadStatus, 13 + getPreloadStatus, 14 + } from "~/components/console/query-trace-utils"; 5 15 import { PaginationNav } from "~/components/pagination-nav"; 6 16 import { ConsoleCard } from "~/components/console/console-card"; 7 17 import { SectionHeader } from "~/components/console/section-header"; ··· 9 19 import { POKEMON_LIMIT } from "~/constants"; 10 20 import { getPokemonListQueryKey, getPokemonListQueryFn } from "~/util/pokemon"; 11 21 import { lazily } from "~/util/lazily"; 22 + import { is } from "drizzle-orm"; 12 23 13 24 const { PokemonTable } = lazily(() => import("~/components/console/pokemon-table")); 14 25 15 26 const searchParamsSchema = v.object({ 16 27 offset: v.optional(v.number(), 0), 28 + }); 29 + 30 + const getPreloadingQueryTraceProps = (currentOffset: number) => ({ 31 + behaviorDescription: 32 + "Route-level preload: the loader starts the Pokémon query before this route renders, so the table can use cached data on arrival.", 33 + queryKeys: [formatPokemonListQueryKey("preloading", currentOffset)], 34 + strategyDescription: "loader prefetchQuery", 17 35 }); 18 36 19 37 export const Route = createFileRoute("/preloading")({ ··· 52 70 National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 53 71 </h1> 54 72 <div className="min-h-[500px]"> 55 - <Suspense fallback={<PokemonTableSkeleton rowCount={POKEMON_LIMIT} />}> 56 - <PokemonTableContent /> 73 + <Suspense 74 + fallback={ 75 + <> 76 + <QueryTrace 77 + {...getPreloadingQueryTraceProps(currentOffset)} 78 + cacheStatus={getLoadingCacheStatus()} 79 + fetchStatus={getLoadingFetchStatus()} 80 + preloadStatus={getLoadingPreloadStatus()} 81 + /> 82 + <PokemonTableSkeleton rowCount={POKEMON_LIMIT} /> 83 + </> 84 + } 85 + > 86 + <PokemonTableContent currentOffset={currentOffset} /> 57 87 </Suspense> 58 88 </div> 59 89 <PaginationNavOutlet /> ··· 63 93 ); 64 94 } 65 95 66 - function PokemonTableContent() { 96 + function PokemonTableContent({ currentOffset }: { currentOffset: number }) { 67 97 const { pokemonListOptions } = useRouteContext({ from: "/preloading" }); 68 - const { data } = useSuspenseQuery(pokemonListOptions); 98 + const { data, dataUpdatedAt, fetchStatus, status } = useSuspenseQuery(pokemonListOptions); 69 99 70 - return <PokemonTable pokemon={data.pokemon} />; 100 + return ( 101 + <> 102 + <QueryTrace 103 + {...getPreloadingQueryTraceProps(currentOffset)} 104 + cacheStatus={getCacheStatus(dataUpdatedAt)} 105 + fetchStatus={getFetchStatus(fetchStatus, status)} 106 + preloadStatus={getPreloadStatus(dataUpdatedAt)} 107 + /> 108 + <PokemonTable pokemon={data.pokemon} /> 109 + </> 110 + ); 71 111 } 72 112 73 113 function PaginationNavOutlet() { 74 114 const { pokemonListOptions } = useRouteContext({ from: "/preloading" }); 75 - const { data, isPending } = useQuery(pokemonListOptions); 115 + const { data } = useQuery(pokemonListOptions); 76 116 77 117 return ( 78 118 <PaginationNav 79 - prevOffset={isPending ? undefined : (data?.prevOffset ?? undefined)} 80 - nextOffset={isPending ? undefined : (data?.nextOffset ?? undefined)} 119 + prevOffset={data?.prevOffset ?? null} 120 + nextOffset={data?.nextOffset ?? null} 81 121 to="/preloading" 82 122 /> 83 123 );