Async Loading States
React has Suspense. Solid has <Suspense> and <Show>. These handle async loading states declaratively.
Rimitive handles async state explicitly through two primitives: resource for reactive data fetching with automatic refetch, and load for simpler async boundaries. Both use match for rendering. No magic, no special components—just reactive state.
The Resource Pattern
Section titled “The Resource Pattern”The resource primitive wraps async operations and exposes their state reactively:
const ProductList = (svc: Service) => { const { el, resource, match } = svc;
const products = resource((signal) => fetch('/api/products', { signal }).then(r => r.json()) );
// products() returns { status: 'pending' | 'ready' | 'error', value?, error? }};Rendering Different States
Section titled “Rendering Different States”Use match to render based on state:
return match(products, (state) => { switch (state.status) { case 'pending': return Spinner(svc); case 'error': return ErrorMessage(svc, state.error); case 'ready': return ProductGrid(svc, state.value); }});Or use the convenience accessors:
return el('div')( match(products.loading, (loading) => loading ? Spinner(svc) : match(products.error, (error) => error ? ErrorMessage(svc, error) : ProductGrid(svc, products.data()!) ) ));The explicit approach is more verbose but clearer about what’s happening.
A Show Helper
Section titled “A Show Helper”If you want something closer to Solid’s <Show>, create a simple helper:
const show = <T, R>( when: () => T | null | undefined | false, render: (value: T) => R, fallback?: () => R): R | undefined => { const value = when(); if (value) return render(value); return fallback?.();};
// Usagereturn el('div')( show( () => products.data(), (data) => ProductGrid(svc, data), () => Spinner(svc) ));But this is just a function—nothing special about it.
Multiple Resources
Section titled “Multiple Resources”When loading multiple resources, handle them together:
const Dashboard = (svc: Service) => { const { el, resource, computed, match } = svc;
const user = resource((s) => fetchUser(s)); const stats = resource((s) => fetchStats(s)); const notifications = resource((s) => fetchNotifications(s));
// Combine loading states const allLoading = computed(() => user.loading() || stats.loading() || notifications.loading() );
const anyError = computed(() => user.error() || stats.error() || notifications.error() );
return match(allLoading, (loading) => { if (loading) return Spinner(svc);
const error = anyError(); if (error) return ErrorMessage(svc, error);
return el('div')( UserHeader(svc, user.data()!), StatsPanel(svc, stats.data()!), NotificationList(svc, notifications.data()!) ); });};Dependent Resources
Section titled “Dependent Resources”When one resource depends on another:
const ProductDetail = (svc: Service) => { const { el, resource, computed, match } = svc;
const productId = svc.params.id; // from router
const product = resource((s) => fetch(`/api/products/${productId()}`, { signal: s }).then(r => r.json()) );
// Reviews depend on product being loaded const reviews = resource((s) => { const p = product.data(); if (!p) return Promise.resolve([]); // No product yet, return empty
return fetch(`/api/products/${p.id}/reviews`, { signal: s }) .then(r => r.json()); });
return match(product, (state) => { if (state.status !== 'ready') { return state.status === 'pending' ? Spinner(svc) : ErrorMessage(svc, state.error); }
return el('div')( ProductInfo(svc, state.value), match(reviews, (reviewState) => reviewState.status === 'pending' ? el('div')('Loading reviews...') : reviewState.status === 'error' ? el('div')('Failed to load reviews') : ReviewList(svc, reviewState.value) ) ); });};Refetching
Section titled “Refetching”Resources track dependencies and refetch automatically:
const category = signal('electronics');
const products = resource((s) => fetch(`/api/products?category=${category()}`, { signal: s }) .then(r => r.json()));
// Change category -> products automatically refetchescategory('clothing');Manual refetch is also available:
el('button').props({ onclick: () => products.refetch()})('Refresh')The Load Pattern
Section titled “The Load Pattern”For simpler async boundaries—especially in SSR scenarios—use load():
const ProductList = (svc: Service) => { const { el, load, match } = svc;
return load( () => fetch('/api/products').then(r => r.json()), (state) => match(state.status, (status) => { switch (status) { case 'pending': return Spinner(svc); case 'error': return ErrorMessage(svc, state.error()); case 'ready': return ProductGrid(svc, state.data()!); } }) );};The key difference: load() takes a fetcher and a renderer. The renderer receives a state object with reactive properties (status, data, error) that you read by calling them.
When to use each
Section titled “When to use each”Use resource when you need:
- Automatic refetching when dependencies change
- AbortController integration for request cancellation
- A reactive value you can pass around and read anywhere
Use load when you need:
- Simple one-shot data fetching
- SSR streaming with explicit data boundaries
- Direct control over the loading UI in one place
// load() is ideal for SSR - data and UI are co-locatedconst Page = (svc: Service) => { const { el, load, match } = svc;
return el('main')( load( () => fetchPageData(), (state) => match(state.status, (status) => status === 'ready' ? PageContent(svc, state.data()!) : status === 'error' ? ErrorFallback(svc) : LoadingSkeleton(svc) ) ) );};Cleanup
Section titled “Cleanup”Resources should be disposed when no longer needed:
const ProductPanel = (svc: Service) => { const { el, resource } = svc;
const products = resource((s) => fetchProducts(s));
// Cleanup when element is removed return el('div').ref(() => products.dispose)( // ... content );};