Skip to content

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

ts
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" }]
});
OptionTypeDefaultDescription
key(params) => string[]RequiredCache key function. Same key = same cache entry.
fetch(params, ctx) => Promise<T>RequiredAsync fetch function. Receives AbortSignal via ctx.signal.
staleTimestring | number0How long data is fresh. "1m", "5m", "1h", "7d", or ms.
cacheTimestring | number"5m"How long unused data stays in cache before garbage collection.
tagsTag[][]Invalidation tags. Used to invalidate groups of resources.
enabledboolean | (params, state) => booleantrueConditional fetching. When false, returns cached data without fetching.
select(data) => TTransform raw fetched data before caching.
onSuccess(data, params) => voidCalled after successful fetch.
onError(error, params) => voidCalled after failed fetch.
maxCacheEntriesnumberLRU cache eviction limit.

Fetch a Resource

ts
// From code
const data = await products.fetch({ search: "keyboard", page: 1 });

// Via handle
const result = await products.fetch(filters);

Use in React

tsx
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

OptionTypeDescription
refetchOnWindowFocusbooleanRefetch when the window regains focus
refetchOnReconnectbooleanRefetch when the browser reconnects
refetchIntervalnumberPolling interval in ms
keepPreviousDatabooleanKeep previous data visible while fetching new data
placeholderDataTData shown before the first fetch completes
select(data) => TTransform data for this component only

useMeshResource Return

FieldTypeDescription
dataT | undefinedThe fetched (or transformed) data
pendingbooleanTrue during the first fetch
fetchingbooleanTrue during any fetch (including background refetches)
errorError | undefinedThe last fetch error
refetch() => Promise<T>Force a refetch
cancel() => voidCancel the in-flight fetch

Prefetch

Prefetch before navigation or on hover:

tsx
<button onMouseEnter={() => void productsResource.prefetch({ search: "", page: 1 })}>
  View products
</button>

Invalidate by Tag

ts
// Invalidate all resources tagged with { type: "products" }
mesh.invalidate([{ type: "products" }]);

// Invalidate specific resource
mesh.invalidate([{ type: "products", id: "list" }]);

Cancel a Fetch

ts
// Via mesh
mesh.cancelResource("products.list", filters);

// Via handle
productsResource.cancel(filters);

Check Fetching Status

ts
const count = mesh.isFetching();                           // All fetching resources
const cartCount = mesh.isFetching({ names: ["cart"] });    // By name

Suspense Support

Use useSuspenseMeshResource for React Suspense integration:

tsx
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:

ts
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:

ts
// 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

Released under the MIT License.