Ludicord
Guide 16 · Full stack

Data fetching

Load, cache, retry, refresh, and mutate remote data with useActivityQuery.

Ludicord 3.1.0Public source 20c4889

useActivityQuery() loads asynchronous data into a shared browser cache. It deduplicates work by key, retries transient failures, aborts timed-out requests, keeps old data during refresh, and exposes explicit refetch and mutation controls.

#Create a query

app/embeds/profile/embed.tsxtsx
1import { useActivityQuery } from "ludicord/activity";
2
3interface Profile {
4 name: string;
5 wins: number;
6}
7
8const profile = useActivityQuery<Profile>(
9 ["profile", userId],
10 async ({ signal }) => {
11 const response = await fetch(`/api/players/${userId}`, { signal });
12 if (!response.ok) throw new Error("Profile request failed");
13 return response.json() as Promise<Profile>;
14 },
15 { enabled: Boolean(userId), staleTime: 30_000, retries: 2 },
16);

Query keys may be non-empty strings or JSON-serializable arrays. Every component using the same serialized key shares the same cached snapshot and in-flight request.

#Render every status

StatusMeaning
idleDisabled or not loaded yet
loadingLoading without cached data
refreshingLoading while previous data remains available
readyData is available
errorThe latest request failed
tsxtsx
1if (profile.status === "loading") return <ProfileSkeleton />;
2if (profile.status === "error") return <Retry error={profile.error} onRetry={profile.refetch} />;
3if (!profile.data) return null;
4return <ProfileCard profile={profile.data} refreshing={profile.status === "refreshing"} />;

#Configure behavior

OptionDefaultPurpose
enabledtrueDelays work until dependencies exist
initialDatanoneSeeds the cache and ready state
staleTime0Time in milliseconds before a mount refetches
retries1Retry count, from 0 to 10
timeout30000Abort timeout, from 1ms to 900000ms

Retries use a short exponential delay capped at two seconds. The query callback receives an AbortSignal; pass it through so replacement, timeout, and cache clearing can cancel useful work.

#Refetch and mutate

refetch() forces a new request and returns its promise. mutate() updates the cache immediately without contacting the server.

tsxtsx
1await saveProfile(next);
2profile.mutate((current) => current ? { ...current, name: next.name } : next);

Optimistic mutation is not a security boundary. The API route still validates and authorizes the write.

#Clear cached data

tsts
1import { clearActivityQueryCache } from "ludicord/activity";
2
3clearActivityQueryCache(["profile", userId]);

Omit the key to clear every query and abort active work—for example after logout. The cache bounds itself and prefers removing inactive entries when it grows.

#Query or WebSocket?

Use a query for request/response data with a useful stale window. Use a WebSocket when the server must push updates immediately. A common Activity loads an initial snapshot with a query, then applies live event updates through a socket or shared state.

#Next step

Open Configuration to tune the runtime limits and behavior supporting these client and server features.