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.
The Manual Way
Section titled “The Manual Way”You could manage async state yourself with signals and effects:
import { compose } from '@rimitive/core';import { SignalModule, ComputedModule, EffectModule } from '@rimitive/signals/extend';
const { signal, effect } = compose(SignalModule, ComputedModule, EffectModule);
// Stateconst loading = signal(true);const data = signal<string[]>([]);const error = signal<Error | null>(null);
// Fetch on mounteffect(() => { loading(true); fetch('/api/items') .then(r => r.json()) .then(items => { data(items); loading(false); }) .catch(err => { error(err); loading(false); });});This works, but you’re managing three signals, handling errors, and there’s no cancellation. When dependencies change mid-flight, you get race conditions.
The resource() Primitive
Section titled “The resource() Primitive”resource() handles all of that. Add the resource module:
import { compose } from '@rimitive/core';import { SignalModule, ComputedModule, EffectModule } from '@rimitive/signals/extend';import { ResourceModule } from '@rimitive/resource';
const svc = compose( SignalModule, ComputedModule, EffectModule, ResourceModule);
const { signal, resource } = svc;Basic Usage
Section titled “Basic Usage”Create a resource with a fetcher function:
const items = resource((signal) => fetch('/api/items', { signal }).then(r => r.json()));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 objectitems();// { status: 'pending' }// { status: 'ready', value: [...] }// { status: 'error', error: Error }
// Convenience accessorsitems.loading(); // true | falseitems.data(); // T | undefineditems.error(); // unknown | undefinedAll are reactive—use them in computeds or effects.
Reactive Dependencies
Section titled “Reactive Dependencies”Here’s where resources shine. Read signals inside the fetcher, and the resource refetches when they change:
const categoryId = signal(1);
const products = resource((signal) => fetch(`/api/products?category=${categoryId()}`, { signal }) .then(r => r.json()));
// Initial fetch: /api/products?category=1
categoryId(2);// Aborts previous request// New fetch: /api/products?category=2The previous request is automatically aborted. No race conditions, no stale data.
Multiple Dependencies
Section titled “Multiple Dependencies”Track as many signals as you need:
const category = signal('electronics');const sortBy = signal('price');const page = signal(1);
const products = resource((signal) => fetch( `/api/products?category=${category()}&sort=${sortBy()}&page=${page()}`, { signal } ).then(r => r.json()));
// Any change triggers a refetchcategory('books'); // refetchsortBy('rating'); // refetchpage(2); // refetchRendering Resources
Section titled “Rendering Resources”Use match() to render different states:
import { compose } from '@rimitive/core';import { SignalModule, ComputedModule, EffectModule } from '@rimitive/signals/extend';import { ResourceModule } from '@rimitive/resource';import { createDOMAdapter } from '@rimitive/view/adapters/dom';import { createElModule } from '@rimitive/view/el';import { createMatchModule } from '@rimitive/view/match';import { createMapModule } from '@rimitive/view/map';import { MountModule } from '@rimitive/view/deps/mount';
const adapter = createDOMAdapter();
const svc = compose( SignalModule, ComputedModule, EffectModule, ResourceModule, createElModule(adapter), createMatchModule(adapter), createMapModule(adapter), MountModule);
const { el, match, map, resource, mount } = svc;Now render based on resource state:
type Product = { id: number; name: string; price: number };
const products = resource<Product[]>((signal) => fetch('/api/products', { signal }).then(r => r.json()));
const ProductList = () => 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')( map(state.value, (p) => p.id, (product) => el('li')(computed(() => product().name)) ) ); } });Using Convenience Accessors
Section titled “Using Convenience Accessors”For simpler rendering, use the boolean accessors:
const ProductList = () => el('div')( match(products.loading, (isLoading) => isLoading ? el('div')('Loading...') : null ), match(products.error, (err) => err ? el('div')(`Error: ${err}`) : null ), match(products.data, (data) => data ? el('ul')( map(data, (p) => p.id, (product) => el('li')(computed(() => product().name)) ) ) : null ) );Refetch and Dispose
Section titled “Refetch and Dispose”Manual Refetch
Section titled “Manual Refetch”Trigger a refetch programmatically:
const products = resource((signal) => fetch('/api/products', { signal }).then(r => r.json()));
// Later...products.refetch();Useful for “refresh” buttons or after mutations.
Cleanup
Section titled “Cleanup”When a resource is no longer needed, dispose it to abort any in-flight request and stop tracking:
const products = resource((signal) => fetch('/api/products', { signal }).then(r => r.json()));
// When doneproducts.dispose();In components, clean up when the element is removed:
const ProductList = () => { const products = resource((signal) => fetch('/api/products', { signal }).then(r => r.json()) );
return el('div').ref(() => { // Cleanup callback runs when element is removed return () => products.dispose(); })( // ... render products );};A Complete Example
Section titled “A Complete Example”A product browser with category filtering:
import { compose } from '@rimitive/core';import { SignalModule, ComputedModule, EffectModule } from '@rimitive/signals/extend';import type { Reactive } from '@rimitive/signals';import { ResourceModule } from '@rimitive/resource';import { createDOMAdapter } from '@rimitive/view/adapters/dom';import { createElModule } from '@rimitive/view/el';import { createMatchModule } from '@rimitive/view/match';import { createMapModule } from '@rimitive/view/map';import { MountModule } from '@rimitive/view/deps/mount';
const adapter = createDOMAdapter();const svc = compose( SignalModule, ComputedModule, EffectModule, ResourceModule, createElModule(adapter), createMatchModule(adapter), createMapModule(adapter), MountModule);
const { el, match, map, signal, computed, resource, mount } = svc;
// Typestype Category = { id: number; name: string };type Product = { id: number; name: string; price: number };
// Stateconst selectedCategory = signal<number | null>(null);
// Resourcesconst categories = resource<Category[]>((signal) => fetch('/api/categories', { signal }).then(r => r.json()));
const products = resource<Product[]>((signal) => { const catId = selectedCategory(); const url = catId ? `/api/products?category=${catId}` : '/api/products'; return fetch(url, { signal }).then(r => r.json());});
// Componentsconst CategoryButton = (category: Category) => el('button').props({ className: computed(() => selectedCategory() === category.id ? 'active' : '' ), onclick: () => selectedCategory(category.id), })(category.name);
const ProductCard = (productSignal: Reactive<Product>) => el('div').props({ className: 'product' })( el('h3')(computed(() => productSignal().name)), el('p')(computed(() => `$${productSignal().price}`)) );
const App = () => el('div').props({ className: 'app' })( el('h1')('Products'),
// Category filter el('div').props({ className: 'categories' })( el('button').props({ className: computed(() => selectedCategory() === null ? 'active' : '' ), onclick: () => selectedCategory(null), })('All'), match(categories, (state) => state.status === 'ready' ? el('span')( ...state.value.map(CategoryButton) ) : null ) ),
// Products grid match(products, (state) => { switch (state.status) { case 'pending': return el('div').props({ className: 'loading' })('Loading products...'); case 'error': return el('div').props({ className: 'error' })( 'Failed to load products', el('button').props({ onclick: () => products.refetch() })('Retry') ); case 'ready': return state.value.length === 0 ? el('div')('No products found') : el('div').props({ className: 'grid' })( map(state.value, (p) => p.id, ProductCard) ); } }) );
const app = mount(App());document.body.appendChild(app.element!);Reactive dependencies, automatic cancellation, clean rendering. The resource handles the complexity; you handle the UI.
What About SSR?
Section titled “What About SSR?”For server-side rendering with data, Rimitive provides load() which integrates with the SSR system. That’s covered in the SSR guide—for now, resource() handles client-side data fetching.
Next: Adding Routing for navigation between views.