Skip to content

Loading Data

Package on GitHub

Data loading in Rimitive is reactive. When dependencies change, data refetches. When requests overlap, stale ones get cancelled. Let’s build up to that.


resource() manages loading state, errors, and automatic cancellation. Add the resource module to your service:

service.ts
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;

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.

A resource has multiple ways to read its state:

// Full state object
products();
// { status: 'idle' } // when enabled=false
// { status: 'pending' }
// { status: 'ready', value: [...] }
// { status: 'error', error: Error }
// Convenience accessors
products.idle(); // true when disabled
products.loading(); // true when pending
products.data(); // T | undefined
products.error(); // unknown | undefined

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:

  1. The previous request is automatically aborted
  2. A new fetch starts with the updated category

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
);
};
};

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)
);
}
})
);
};
};

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
})
);
};
};

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.


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.

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.


Rimitive has two modules for async data:

resource()load()
Use caseClient-side data fetchingServer-rendered data with hydration
Reactive depsYes—refetches when signals changeNo—fetches once
CancellationAutomatic via AbortSignalNo
SSR supportNoYes—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.