Resources
Resources are cached API/server reads owned by the mesh. They handle the production loop: API call, loading state, cache, request deduplication, invalidation, refetch, pagination, and UI sync.
Define a Resource
const products = mesh.resource("products.list", {
key: (filters: { search: string; page: number }) => ["products", filters],
staleTime: "1m",
cacheTime: "10m",
async fetch(filters, ctx) {
return api.get<Product[]>("/products", {
query: filters,
signal: ctx.signal
});
},
tags: [{ type: "products" }]
});| Option | Type | Default | Description |
|---|---|---|---|
key | (params) => string[] | Required | Cache key function. Same key = same cache entry. |
fetch | (params, ctx) => Promise<T> | Required | Async fetch function. Receives AbortSignal via ctx.signal. |
staleTime | string | number | 0 | How long data is fresh. "1m", "5m", "1h", "7d", or ms. |
cacheTime | string | number | "5m" | How long unused data stays in cache before garbage collection. |
tags | Tag[] | [] | Invalidation tags. Used to invalidate groups of resources. |
enabled | boolean | (params, state) => boolean | true | Conditional fetching. When false, returns cached data without fetching. |
select | (data) => T | — | Transform raw fetched data before caching. |
onSuccess | (data, params) => void | — | Called after successful fetch. |
onError | (error, params) => void | — | Called after failed fetch. |
maxCacheEntries | number | — | LRU cache eviction limit. |
Fetch a Resource
// From code
const data = await products.fetch({ search: "keyboard", page: 1 });
// Via handle
const result = await products.fetch(filters);Use in React
import { useMeshResource } from "statemesh-core";
function ProductList({ filters }: { filters: Filters }) {
const products = useMeshResource(productsResource, filters, {
refetchOnWindowFocus: true,
refetchOnReconnect: true,
refetchInterval: 30000
});
if (products.pending) return <p>Loading...</p>;
if (products.error) return <button onClick={() => products.refetch()}>Retry</button>;
return (
<ul>
{products.data?.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}useMeshResource Options
| Option | Type | Description |
|---|---|---|
refetchOnWindowFocus | boolean | Refetch when the window regains focus |
refetchOnReconnect | boolean | Refetch when the browser reconnects |
refetchInterval | number | Polling interval in ms |
keepPreviousData | boolean | Keep previous data visible while fetching new data |
placeholderData | T | Data shown before the first fetch completes |
select | (data) => T | Transform data for this component only |
useMeshResource Return
| Field | Type | Description |
|---|---|---|
data | T | undefined | The fetched (or transformed) data |
pending | boolean | True during the first fetch |
fetching | boolean | True during any fetch (including background refetches) |
error | Error | undefined | The last fetch error |
refetch | () => Promise<T> | Force a refetch |
cancel | () => void | Cancel the in-flight fetch |
Prefetch
Prefetch before navigation or on hover:
<button onMouseEnter={() => void productsResource.prefetch({ search: "", page: 1 })}>
View products
</button>Invalidate by Tag
// Invalidate all resources tagged with { type: "products" }
mesh.invalidate([{ type: "products" }]);
// Invalidate specific resource
mesh.invalidate([{ type: "products", id: "list" }]);Cancel a Fetch
// Via mesh
mesh.cancelResource("products.list", filters);
// Via handle
productsResource.cancel(filters);Check Fetching Status
const count = mesh.isFetching(); // All fetching resources
const cartCount = mesh.isFetching({ names: ["cart"] }); // By nameSuspense Support
Use useSuspenseMeshResource for React Suspense integration:
import { Suspense } from "react";
import { MeshErrorBoundary, useSuspenseMeshResource } from "statemesh-core";
function Products() {
const products = useSuspenseMeshResource(productsResource, filters);
return products.data.map((p) => <div key={p.id}>{p.name}</div>);
}
function ProductsRoute() {
return (
<MeshErrorBoundary fallbackRender={({ error, reset }) => (
<button onClick={reset}>{error.message}: retry</button>
)}>
<Suspense fallback={<p>Loading products...</p>}>
<Products />
</Suspense>
</MeshErrorBoundary>
);
}useSuspenseMeshResource throws the shared in-flight promise only when no cache data exists. Cached data stays visible during background updates.
Pagination
For paginated APIs, use getNextPageParam and fetchNextPage:
const feed = mesh.resource("feed.pages", {
async fetch(_params, ctx) {
return api.get<FeedPage>("/feed", {
query: { cursor: String(ctx.pageParam ?? "") }
});
},
getNextPageParam: (lastPage) => lastPage.nextCursor,
mergePages: (pages) => ({
items: pages.flatMap((page) => page.items),
nextCursor: pages.at(-1)?.nextCursor ?? null
}),
tags: ["feed"]
});Dehydrate & Hydrate
For SSR, tests, or cache transfer:
// Snapshot resource cache
const snapshot = mesh.dehydrateResources({ tags: [{ type: "products" }] });
// Restore on client
mesh.hydrateResources(window.__STATEMESH_RESOURCES__);Important Notes
TIP
Resources deduplicate in-flight requests by key. Multiple components fetching the same resource with the same params share a single network request.
TIP
When enabled returns false, the resource returns cached data without fetching. Useful for conditional data loading (e.g., only fetch when authenticated).
WARNING
The fetch function receives an AbortSignal via ctx.signal. Always pass it to your HTTP client so cancelled requests don't update the cache.
Next Steps
- Mutations — API writes with optimistic rollback
- API Client — HTTP client with auth and retry
- Persistence — Cache and state persistence
