Skip to content

Commit 9bd8c98

Browse files
committed
fix(landing/QueryLanding): restore the live cache demo with a real 'useQuery' hero
1 parent 797555e commit 9bd8c98

2 files changed

Lines changed: 307 additions & 2 deletions

File tree

src/components/landing/LibraryLanding.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ export type LibraryLandingConfig = {
6666
items: readonly LibraryLandingWorkbenchItem[]
6767
label: string
6868
}
69+
// Replaces the static `hero` mockup with a live panel. Query uses this to
70+
// demo its own cache; every other landing rides the mockup.
71+
heroRender?: React.ReactNode
6972
libraryId: LibraryLandingId
7073
lifecycle: {
7174
body: string
@@ -189,7 +192,7 @@ export function LibraryLanding({ config }: { config: LibraryLandingConfig }) {
189192
<LibraryLandingShell
190193
description={config.description}
191194
headline={config.headline}
192-
hero={<LandingWorkbench config={config.hero} />}
195+
hero={config.heroRender ?? <LandingWorkbench config={config.hero} />}
193196
libraryId={config.libraryId}
194197
prompt={config.prompt}
195198
promptLabel={config.promptLabel}

src/components/landing/QueryLanding.tsx

Lines changed: 303 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,67 @@
1+
import * as React from 'react'
2+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
13
import {
4+
ArrowsClockwiseIcon,
25
EyeClosedIcon,
36
KeyIcon,
47
LightningIcon,
8+
PlusIcon,
59
SkullIcon,
610
} from '@phosphor-icons/react'
711

12+
import { usePrefersReducedMotion } from '~/utils/usePrefersReducedMotion'
813
import { LibraryLanding, type LibraryLandingConfig } from './LibraryLanding'
914

15+
type QueryHeroIssue = {
16+
id: string
17+
observers: number
18+
priority: number
19+
title: string
20+
}
21+
22+
type QueryHeroSnapshot = {
23+
fetchedAt: number
24+
revision: number
25+
rows: Array<QueryHeroIssue>
26+
}
27+
28+
type QueryHeroMutationContext = {
29+
previous?: QueryHeroSnapshot
30+
}
31+
32+
const queryHeroKey = ['landing-query-hero'] as const
33+
34+
const queryHeroInitialRows: Array<QueryHeroIssue> = [
35+
{ id: 'router-cache', observers: 3, priority: 98, title: 'Router dashboard' },
36+
{ id: 'project-detail', observers: 2, priority: 91, title: 'Project detail' },
37+
{
38+
id: 'offline-queue',
39+
observers: 1,
40+
priority: 84,
41+
title: 'Offline mutation queue',
42+
},
43+
]
44+
45+
const queryHeroInitialSnapshot: QueryHeroSnapshot = {
46+
// `0` keeps `Date.now()` out of the first render so SSR and hydration agree.
47+
fetchedAt: 0,
48+
revision: 0,
49+
rows: queryHeroInitialRows,
50+
}
51+
52+
const queryHeroMutationTitles = [
53+
'Optimistic table edit',
54+
'Search filter sync',
55+
'Background retry lane',
56+
'Prefetched route data',
57+
]
58+
59+
function waitForQueryHero(ms: number) {
60+
return new Promise<void>((resolve) => {
61+
setTimeout(resolve, ms)
62+
})
63+
}
64+
1065
const queryLanding = {
1166
libraryId: 'query',
1267
headline: 'The server-state standard for modern frontend apps.',
@@ -115,5 +170,252 @@ const queryLanding = {
115170
} satisfies LibraryLandingConfig
116171

117172
export default function QueryLanding() {
118-
return <LibraryLanding config={queryLanding} />
173+
return (
174+
<LibraryLanding
175+
config={{ ...queryLanding, heroRender: <QueryCachePanel /> }}
176+
/>
177+
)
178+
}
179+
180+
/**
181+
* The hero panel runs a real QueryClient rather than mocking one: the cache
182+
* badge, revision counter, and row list are all derived from query state, and
183+
* "Add issue" is an optimistic mutation that rolls back on error.
184+
*/
185+
function QueryCachePanel() {
186+
const prefersReducedMotion = usePrefersReducedMotion()
187+
const queryClient = useQueryClient()
188+
const serverRowsRef = React.useRef(queryHeroInitialRows)
189+
const serverRevisionRef = React.useRef(0)
190+
const mutationSequenceRef = React.useRef(0)
191+
// Starts paused so the server render matches the first client render; the
192+
// effect below turns it on unless the visitor asked for reduced motion.
193+
const [isLive, setIsLive] = React.useState(false)
194+
195+
const projectsQuery = useQuery({
196+
queryKey: queryHeroKey,
197+
queryFn: async (): Promise<QueryHeroSnapshot> => {
198+
await waitForQueryHero(620)
199+
200+
return {
201+
fetchedAt: Date.now(),
202+
revision: serverRevisionRef.current,
203+
rows: serverRowsRef.current,
204+
}
205+
},
206+
initialData: queryHeroInitialSnapshot,
207+
initialDataUpdatedAt: 0,
208+
refetchInterval: isLive ? 4200 : false,
209+
staleTime: 3200,
210+
})
211+
212+
const addIssueMutation = useMutation<
213+
QueryHeroIssue,
214+
Error,
215+
QueryHeroIssue,
216+
QueryHeroMutationContext
217+
>({
218+
mutationFn: async (issue) => {
219+
await waitForQueryHero(720)
220+
serverRevisionRef.current += 1
221+
serverRowsRef.current = [
222+
issue,
223+
...serverRowsRef.current.filter((row) => row.id !== issue.id),
224+
].slice(0, 5)
225+
226+
return issue
227+
},
228+
onMutate: async (issue) => {
229+
await queryClient.cancelQueries({ queryKey: queryHeroKey })
230+
const previous = queryClient.getQueryData<QueryHeroSnapshot>(queryHeroKey)
231+
232+
queryClient.setQueryData<QueryHeroSnapshot>(queryHeroKey, (current) => ({
233+
fetchedAt: current?.fetchedAt ?? 0,
234+
revision: current?.revision ?? serverRevisionRef.current,
235+
rows: [
236+
issue,
237+
...(current?.rows ?? queryHeroInitialRows).filter(
238+
(row) => row.id !== issue.id,
239+
),
240+
].slice(0, 5),
241+
}))
242+
243+
return { previous }
244+
},
245+
onError: (_error, _issue, context) => {
246+
if (context?.previous) {
247+
queryClient.setQueryData<QueryHeroSnapshot>(
248+
queryHeroKey,
249+
context.previous,
250+
)
251+
}
252+
},
253+
onSettled: () => queryClient.invalidateQueries({ queryKey: queryHeroKey }),
254+
})
255+
256+
const cacheState = projectsQuery.isFetching
257+
? 'fetching'
258+
: projectsQuery.isStale
259+
? 'stale'
260+
: 'fresh'
261+
const fetchedLabel =
262+
projectsQuery.data.fetchedAt > 0
263+
? `${Math.max(0, Math.round((Date.now() - projectsQuery.data.fetchedAt) / 1000))}s ago`
264+
: 'primed'
265+
266+
React.useEffect(() => {
267+
if (prefersReducedMotion === false) {
268+
setIsLive(true)
269+
}
270+
}, [prefersReducedMotion])
271+
272+
const addIssue = () => {
273+
const nextSequence = mutationSequenceRef.current + 1
274+
const nextTitle =
275+
queryHeroMutationTitles[
276+
(nextSequence - 1) % queryHeroMutationTitles.length
277+
]
278+
279+
mutationSequenceRef.current = nextSequence
280+
addIssueMutation.mutate({
281+
id: `optimistic-${nextSequence}`,
282+
observers: (nextSequence % 3) + 1,
283+
priority: 72 + ((nextSequence * 7) % 24),
284+
title: nextTitle ?? 'Optimistic write',
285+
})
286+
}
287+
288+
return (
289+
<div className="library-landing-graphic min-w-0 overflow-hidden rounded-xl border border-[color:rgb(var(--landing-glow)/0.45)] bg-background-surface shadow-[0_24px_70px_-28px_rgb(var(--landing-glow)/0.45)] dark:shadow-[inset_-3px_-4px_18px_-7px_var(--landing-accent),0_24px_70px_rgb(0_0_0/0.18)]">
290+
<div className="flex items-center justify-between border-b border-border-subtle px-4 py-3">
291+
<div aria-hidden="true" className="flex gap-1.5">
292+
<span className="size-2.5 rounded-full bg-[#ff5f57]" />
293+
<span className="size-2.5 rounded-full bg-[#febc2e]" />
294+
<span className="size-2.5 rounded-full bg-[#28c840]" />
295+
</div>
296+
<span className="font-ds-mono text-ds-mono-caps-xs uppercase text-text-primary/65">
297+
{queryLanding.hero.label}
298+
</span>
299+
</div>
300+
301+
<div className="grid min-h-[22rem] lg:grid-cols-[1.08fr_0.82fr]">
302+
<div className="space-y-3 border-border-subtle p-4 lg:border-r">
303+
<div className="mb-4 flex flex-wrap items-center gap-2 font-ds-mono text-ds-mono-caps-xs uppercase">
304+
<span
305+
className={
306+
cacheState === 'fresh'
307+
? 'rounded-sm bg-emerald-500 px-2 py-1 text-emerald-950'
308+
: cacheState === 'fetching'
309+
? 'rounded-sm bg-amber-400 px-2 py-1 text-amber-950'
310+
: 'rounded-sm bg-[var(--landing-accent)] px-2 py-1 text-[var(--landing-accent-ink)]'
311+
}
312+
>
313+
{cacheState}
314+
</span>
315+
<span className="rounded-sm bg-text-primary/5 px-2 py-1 text-text-primary/35">
316+
rev {projectsQuery.data.revision} / {fetchedLabel}
317+
</span>
318+
</div>
319+
320+
{projectsQuery.data.rows.map((row) => (
321+
<div
322+
key={row.id}
323+
className="block w-full rounded-lg border border-transparent bg-background-subtle p-4 text-left"
324+
>
325+
<span className="flex items-start justify-between gap-4">
326+
<span className="min-w-0">
327+
<span className="block truncate font-ds-mono text-ds-mono-xs text-text-primary">
328+
['issues', '{row.id}']
329+
</span>
330+
<span className="mt-1 block text-ds-body-xs text-text-primary/45">
331+
{row.title}
332+
</span>
333+
</span>
334+
<span className="shrink-0 rounded bg-[var(--landing-accent)] px-2 py-1 font-ds-mono text-ds-mono-2xs text-[var(--landing-accent-ink)]">
335+
P{row.priority}
336+
</span>
337+
</span>
338+
<span className="mt-4 flex items-center gap-3">
339+
<span className="h-1 flex-1 overflow-hidden rounded-full bg-text-primary/5">
340+
<span
341+
className="block h-full rounded-full bg-[var(--landing-accent)] transition-[width] duration-500 motion-reduce:transition-none"
342+
style={{ width: `${row.priority}%` }}
343+
/>
344+
</span>
345+
<span className="font-ds-mono text-ds-mono-caps-xs uppercase text-text-primary/35">
346+
{row.observers} obs
347+
</span>
348+
</span>
349+
</div>
350+
))}
351+
</div>
352+
353+
<div className="flex flex-col p-5">
354+
<div className="flex flex-wrap items-center justify-between gap-2">
355+
<button
356+
type="button"
357+
aria-pressed={isLive}
358+
className="rounded-md bg-[#ff5f5f] px-3 py-2 text-ds-label-sm text-white transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white"
359+
onClick={() => setIsLive((current) => !current)}
360+
>
361+
Live {isLive ? 'on' : 'off'}
362+
</button>
363+
<div className="flex flex-wrap items-center gap-2">
364+
<button
365+
type="button"
366+
className="inline-flex items-center gap-1.5 rounded-md border border-border-subtle px-3 py-2 text-ds-label-sm text-text-primary/70 transition-colors hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--landing-accent-bright)]"
367+
onClick={() => projectsQuery.refetch()}
368+
>
369+
<ArrowsClockwiseIcon
370+
aria-hidden="true"
371+
size={13}
372+
weight="bold"
373+
className={projectsQuery.isFetching ? 'animate-spin' : ''}
374+
/>
375+
Refetch
376+
</button>
377+
<button
378+
type="button"
379+
disabled={addIssueMutation.isPending}
380+
className="inline-flex items-center gap-1.5 rounded-md bg-[#ff5f5f] px-3 py-2 text-ds-label-sm text-white transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white disabled:cursor-wait disabled:opacity-70"
381+
onClick={addIssue}
382+
>
383+
<PlusIcon aria-hidden="true" size={13} weight="bold" />
384+
{queryLanding.hero.actionLabel}
385+
</button>
386+
</div>
387+
</div>
388+
389+
<div className="mt-7" aria-live="polite">
390+
<p className="text-ds-heading-4">{queryLanding.hero.detailTitle}</p>
391+
<p className="mt-2 truncate font-ds-mono text-ds-mono-xs text-[var(--landing-accent-bright)]">
392+
['issues', '{projectsQuery.data.rows[0]?.id ?? 'router-cache'}']
393+
</p>
394+
<p className="mt-4 text-ds-body-sm text-text-primary/55">
395+
{queryLanding.hero.detailBody}
396+
</p>
397+
</div>
398+
399+
<dl className="mt-auto space-y-2 rounded-lg bg-background-subtle p-4 text-ds-body-xs">
400+
{[
401+
{ label: 'status', value: projectsQuery.status },
402+
{
403+
label: 'isFetching',
404+
value: String(projectsQuery.isFetching),
405+
},
406+
{ label: 'staleTime', value: '3,200' },
407+
{ label: 'mutation', value: addIssueMutation.status },
408+
].map((fact) => (
409+
<div key={fact.label} className="flex justify-between gap-3">
410+
<dt className="text-text-primary/45">{fact.label}</dt>
411+
<dd className="text-right font-ds-mono text-ds-mono-xs text-text-primary/85">
412+
{fact.value}
413+
</dd>
414+
</div>
415+
))}
416+
</dl>
417+
</div>
418+
</div>
419+
</div>
420+
)
119421
}

0 commit comments

Comments
 (0)