Skip to content

Data Fetching

The query() method is the primary way to fetch data with λ Query. It handles caching, expiration, stale-while-revalidate, and request deduplication automatically.

Pass a cache key to query(). By default, the key is used as the URL with the global fetch():

import { createQuery } from "@studiolambda/query";
const query = createQuery();
const posts = await query.query("/api/posts");
const user = await query.query("/api/users/42");

The returned promise resolves to the parsed JSON response.

Override the fetcher for a specific query call when the default behavior doesn’t fit:

const user = await query.query("my-user", {
async fetcher(key, { signal }) {
if (key === "my-user") {
const response = await fetch("/api/me", { signal });
if (!response.ok) {
throw new Error("Unable to fetch: " + response.statusText);
}
return response.json();
}
throw new Error("Unknown key");
},
});

When query() triggers a fetch, the in-flight promise is stored in the resolvers cache. Any subsequent query() call for the same key while the fetch is still pending will receive the same promise — no duplicate network requests:

// These three calls result in a single fetch request.
const [a, b, c] = await Promise.all([
query.query("/api/posts"),
query.query("/api/posts"),
query.query("/api/posts"),
]);
// a === b === c (same resolved value, same promise)

This is especially useful in React where multiple components may call useQuery with the same key during the same render cycle.

Force a fresh fetch that bypasses the cache entirely:

const fresh = await query.query("/api/posts", { fresh: true });

With fresh: true, the cache is ignored — a new request is always made, and the result replaces any cached value.

When a cached item has expired, the behavior depends on the stale option:

The expired value is returned immediately, and a background refetch starts. When the refetch completes, a resolved event fires so subscribers can update:

const query = createQuery({ expiration: () => 1000, stale: true });
const first = await query.query("/api/posts"); // Fresh fetch.
await delay(1500); // Wait for expiration.
const second = await query.query("/api/posts"); // Returns stale data instantly.
// Meanwhile, a background refetch is running.
// When it completes, subscribers are notified.

The query waits for the refetch to complete before returning:

const fresh = await query.query("/api/posts", { stale: false });
// Always returns fresh data, but may take longer.

Cancel in-flight requests using the abort() method. This triggers the AbortSignal passed to the fetcher:

// Start a fetch.
const promise = query.query("/api/large-dataset");
// Cancel it.
query.abort("/api/large-dataset");

The abort() method accepts several argument forms:

// Abort a single key.
query.abort("/api/posts");
// Abort a single key with a reason.
query.abort("/api/posts", new Error("User cancelled"));
// Abort multiple keys.
query.abort(["/api/posts", "/api/users"]);
// Abort all in-flight requests.
query.abort();

After aborting, the resolver is removed from the cache. The abort signal is passed to your fetcher, so make sure you use it:

const query = createQuery({
async fetcher(key, { signal }) {
const response = await fetch(key, { signal }); // Abortable.
return response.json();
},
});

When a fetch fails (the promise rejects), the behavior depends on the removeOnError option:

  • removeOnError: false (default): The cached item (if any) is preserved. The error is emitted via the error event.
  • removeOnError: true: The cached item is removed on error, forcing a fresh fetch on the next query.
const query = createQuery({ removeOnError: true });
try {
const data = await query.query("/api/unreliable");
} catch (error) {
console.error("Fetch failed:", error);
}

For background refetches (stale-while-revalidate), errors are silenced by default — the stale data remains in the cache and the error event fires for any subscribers listening.

Get the current cached value for a key without triggering a fetch:

const cached = await query.snapshot("/api/posts");
if (cached) {
console.log("Have cached data:", cached);
} else {
console.log("No data cached for this key");
}

snapshot() returns undefined if the key is not in the items cache.

const itemKeys = query.keys("items"); // Keys with cached data.
const resolverKeys = query.keys("resolvers"); // Keys with in-flight requests.

Check when a cached item expires:

const expiresAt = query.expiration("/api/posts");
if (expiresAt) {
console.log("Expires at:", expiresAt.toISOString());
console.log("Is expired:", expiresAt < new Date());
}

Returns undefined if the key is not in the items cache.