Loading Data
Data loading in Rimitive is reactive. When dependencies change, data refetches. When requests overlap, stale ones get cancelled. Let’s build up to that.
Using resource()
Section titled “Using resource()”resource() manages loading state, errors, and automatic cancellation. Add the resource module to your service:
import { compose } from '@rimitive/core';import { SignalModule, ComputedModule, EffectModule } from '@rimitive/signals/extend';import { ResourceModule } from '@rimitive/resource';import { createElModule } from '@rimitive/view/el';import { createMatchModule } from '@rimitive/view/match';import { createDOMAdapter } from '@rimitive/view/adapters/dom';
const adapter = createDOMAdapter();
export const svc = compose( SignalModule, ComputedModule, EffectModule, ResourceModule, createElModule(adapter), createMatchModule(adapter));
export type Service = typeof svc;Basic Usage
Section titled “Basic Usage”Create a resource inside your component with a fetcher function:
const ProductList = ({ resource, el, match }: Service) => { return () => { const products = resource<Product[]>((signal) => fetch('/api/products', { signal }).then(r => r.json()) );
return el('div').ref(() => products.dispose)( match(products, (state) => { switch (state.status) { case 'pending': return el('div')('Loading...'); case 'error': return el('div')(`Error: ${state.error}`); case 'ready': return el('ul')( ...state.value.map(p => el('li')(p.name)) ); } }) ); };};The fetcher receives an AbortSignal - pass it to fetch() for automatic cancellation.
Reading State
Section titled “Reading State”A resource has multiple ways to read its state:
// Full state objectproducts();// { status: 'idle' } // when enabled=false// { status: 'pending' }// { status: 'ready', value: [...] }// { status: 'error', error: Error }
// Convenience accessorsproducts.idle(); // true when disabledproducts.loading(); // true when pendingproducts.data(); // T | undefinedproducts.error(); // unknown | undefinedReactive Dependencies
Section titled “Reactive Dependencies”Read signals inside the fetcher, and the resource refetches when they change:
const ProductList = (svc: Service) => { const { signal, resource, el, match } = svc;
return (initialCategory: string) => { const category = signal(initialCategory);
const products = resource<Product[]>((signal) => fetch(`/api/products?category=${category()}`, { signal }) .then(r => r.json()) );
return el('div').ref(() => () => products.dispose())( el('div')( el('button').props({ onclick: () => category('electronics') })('Electronics'), el('button').props({ onclick: () => category('books') })('Books') ), match(products, (state) => { // ... render based on state }) ); };};When category changes:
- The previous request is automatically aborted
- A new fetch starts with the updated category
Multiple Dependencies
Section titled “Multiple Dependencies”Track as many signals as you need:
const ProductList = (svc: Service) => { const { signal, resource } = svc;
return () => { const category = signal('electronics'); const sortBy = signal('price'); const page = signal(1);
const products = resource<Product[]>((signal) => fetch( `/api/products?category=${category()}&sort=${sortBy()}&page=${page()}`, { signal } ).then(r => r.json()) );
return el('div').ref(() => () => products.dispose())( // ... render with filters and pagination controls ); };};Rendering Resources
Section titled “Rendering Resources”Use match() to render based on resource state:
const UserProfile = (svc: Service) => { const { resource, el, match, computed } = svc;
return (userId: string) => { const user = resource<User>((signal) => fetch(`/api/users/${userId}`, { signal }).then(r => r.json()) );
return el('div').ref(() => () => user.dispose())( match(user, (state) => { switch (state.status) { case 'idle': return null; // Resource disabled case 'pending': return el('div').props({ className: 'skeleton' })('Loading profile...'); case 'error': return el('div').props({ className: 'error' })( `Failed to load: ${state.error}` ); case 'ready': return el('div')( el('h1')(state.value.name), el('p')(state.value.email) ); } }) ); };};Refetch and Dispose
Section titled “Refetch and Dispose”Manual Refetch
Section titled “Manual Refetch”Trigger a refetch programmatically (useful for “refresh” buttons or after mutations):
const ProductList = (svc: Service) => { const { resource, el, match } = svc;
return () => { const products = resource<Product[]>((signal) => fetch('/api/products', { signal }).then(r => r.json()) );
return el('div').ref(() => () => products.dispose())( el('button').props({ onclick: () => products.refetch() })('Refresh'), match(products, (state) => { // ... render }) ); };};Cleanup
Section titled “Cleanup”Always dispose resources when the component unmounts. The .ref() callback pattern works well:
return el('div').ref(() => { // Return cleanup function return () => products.dispose();})( // ... children);This aborts any in-flight request and stops dependency tracking.
Options
Section titled “Options”enabled
Section titled “enabled”Control whether the resource fetches with the enabled option:
const ProductList = (svc: Service) => { const { signal, resource, el } = svc;
return (selectedId: string | null) => { const id = signal(selectedId);
// Only fetch when we have an ID const product = resource<Product>( (s) => fetch(`/api/products/${id()}`, { signal: s }).then(r => r.json()), { enabled: () => id() !== null } );
// product.idle() is true when disabled return el('div').ref(() => () => product.dispose())( product.idle() ? el('p')('Select a product') : el('p')(product.data()?.name ?? 'Loading...') ); };};enabled accepts a boolean or a reactive function. When false, the resource stays in idle state. When it becomes true, fetching begins.
Lazy Fetching
Section titled “Lazy Fetching”Use enabled to defer fetching until a user action:
const ReportViewer = (svc: Service) => { const { signal, resource, el, match } = svc;
return (reportId: string) => { const shouldLoad = signal(false);
const report = resource<Report>( (s) => fetch(`/api/reports/${reportId}`, { signal: s }).then(r => r.json()), { enabled: shouldLoad } );
return el('div').ref(() => () => report.dispose())( match(report, (state) => { if (state.status === 'idle') { return el('button').props({ onclick: () => shouldLoad(true) })( 'Load Report' ); } if (state.status === 'pending') return el('p')('Loading...'); if (state.status === 'error') return el('p')(`Error: ${state.error}`); return el('div')(/* render report */); }) ); };};The resource stays idle until the user clicks “Load Report”, then fetches.
Control when refetches execute with the flush option:
import { mt, debounce } from '@rimitive/signals/strategies';
// Defer refetches to microtask (coalesces rapid updates)const products = resource(fetcher, { flush: mt });
// Debounce refetches (useful for search-as-you-type)const results = resource( (s) => fetch(`/api/search?q=${query()}`, { signal: s }).then(r => r.json()), { flush: (run) => debounce(300, run) });Without flush, refetches are synchronous—each dependency change triggers an immediate fetch. With a flush strategy, rapid updates are coalesced.
resource() vs load()
Section titled “resource() vs load()”Rimitive has two modules for async data:
resource() | load() | |
|---|---|---|
| Use case | Client-side data fetching | Server-rendered data with hydration |
| Reactive deps | Yes—refetches when signals change | No—fetches once |
| Cancellation | Automatic via AbortSignal | No |
| SSR support | No | Yes—data serializes for hydration |
Use resource() when:
- Building a client-only app
- Data depends on user interaction (filters, pagination, search)
- You need automatic refetching and cancellation
Use load() when:
- You’re doing SSR and want data in the initial HTML
- The data should be fetched on the server and reused on the client
- SEO or first-paint performance matters for this data
In an SSR app, you might use both: load() for the initial page data that should be server-rendered, and resource() for dynamic data that loads after user interaction.
See SSR with Data Loading for load() usage.