Why Signalium?
Fetchium works great with plain React hooks via useQuery. You can use it in any function component without learning anything about signals or reactive programming. But Fetchium is built on Signalium, and opting into Signalium's reactive model unlocks additional capabilities --- automatic memoization, reactive composition, fine-grained reactivity, and natural async/await support.
This page explains what you gain by using Signalium's reactive primitives alongside Fetchium, and when it makes sense to reach for them.
Two Modes
Fetchium supports two approaches to data fetching. You can use either one, or mix them in the same application.
React hooks mode
Use useQuery inside regular function components. This is the simplest approach and works with existing React patterns, state management, and component libraries.
import { useQuery } from 'fetchium/react';
function UserProfile({ userId }: { userId: number }) {
const result = useQuery(GetUser, { id: userId });
if (!result.isReady) return <div>Loading...</div>;
if (result.isRejected) return <div>Error: {result.error.message}</div>;
return (
<div>
<h1>{result.value.name}</h1>
<p>{result.value.email}</p>
</div>
);
}
This mode gives you automatic caching, deduplication, entity normalization, and live data --- all the core Fetchium features. For many applications, this is all you need.
Signalium mode
Wrap your components with component() from Signalium and use fetchQuery() directly. This gives you full reactive composition, automatic memoization, and fine-grained dependency tracking.
import { component } from 'signalium/react';
import { fetchQuery } from 'fetchium';
const UserProfile = component(({ userId }: { userId: number }) => {
const user = fetchQuery(GetUser, { id: userId });
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
});
The difference is subtle in simple cases, but becomes significant as your data requirements grow.
What Signalium Adds
Automatic memoization
Signalium's component() wrapper automatically memoizes your component. It only re-renders when the specific reactive values it reads actually change --- not on every parent re-render.
With standard React, parent re-renders cascade to all children unless you manually wrap components in React.memo and memoize props with useMemo and useCallback. With Signalium, this optimization is automatic:
// This component only re-renders when `user.name` or `user.email` changes.
// Parent re-renders are ignored unless they change the `userId` prop.
const UserProfile = component(({ userId }: { userId: number }) => {
const user = fetchQuery(GetUser, { id: userId });
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
});
This is especially valuable in list views and deeply nested component trees, where unnecessary re-renders are a common performance problem.
Reactive composition
With fetchQuery() directly, you can compose queries inside reactive functions that live outside of components:
import { reactive } from 'signalium';
import { fetchQuery } from 'fetchium';
const getUserWithPosts = reactive((userId: number) => {
const user = fetchQuery(GetUser, { id: userId });
const posts = fetchQuery(GetUserPosts, { userId });
return { user, posts };
});
This reactive function is cached and shared. If multiple components call getUserWithPosts(42), they all read from the same cached computation. The queries are deduplicated, and the derived result is computed once.
This is powerful for building a data layer that sits between your API and your components:
// data/user.ts --- shared reactive data layer
import { reactive } from 'signalium';
import { fetchQuery } from 'fetchium';
export const getFullUser = reactive((userId: number) => {
const user = fetchQuery(GetUser, { id: userId });
const posts = fetchQuery(GetUserPosts, { userId });
const followers = fetchQuery(GetFollowers, { userId });
return {
user,
posts,
followers,
postCount: posts.length,
isPopular: followers.length > 1000,
};
});
// components/UserProfile.tsx
const UserProfile = component(({ userId }: { userId: number }) => {
const { user, isPopular } = getFullUser(userId);
return (
<div>
<h1>
{user.name} {isPopular ? '(Popular)' : ''}
</h1>
</div>
);
});
// components/UserPosts.tsx
const UserPosts = component(({ userId }: { userId: number }) => {
const { posts, postCount } = getFullUser(userId);
return (
<div>
<h2>{postCount} Posts</h2>
{posts.map((post) => (
<PostCard key={post.id} post={post} />
))}
</div>
);
});
Both components call getFullUser(userId), but the queries execute only once. The reactive function caches its result and returns the same object to all consumers.
Fine-grained reactivity
Fetchium entities are reactive Proxy objects. When you access a property on an entity, Signalium tracks that access as a dependency. Only the specific properties your component reads trigger re-renders.
const UserAvatar = component(({ userId }: { userId: number }) => {
const user = fetchQuery(GetUser, { id: userId });
// Only reads `avatarUrl` --- changes to `name`, `email`, etc. do NOT
// cause this component to re-render
return <img src={user.avatarUrl} />;
});
const UserName = component(({ userId }: { userId: number }) => {
const user = fetchQuery(GetUser, { id: userId });
// Only reads `name` --- changes to `avatarUrl`, `email`, etc. do NOT
// cause this component to re-render
return <span>{user.name}</span>;
});
Both components fetch the same user entity (deduplicated by the cache), but they re-render independently based on which properties they actually read. If a mutation updates user.name, only UserName re-renders. UserAvatar is unaffected.
This property-level tracking works automatically through Signalium's reactive system. There is no need to select specific fields or use selector functions.
Async/await support
Signalium's Babel transform rewrites async reactive functions to use generators internally, enabling pause/resume semantics. This lets you await reactive promises naturally --- including query results:
import { reactive } from 'signalium';
import { fetchQuery } from 'fetchium';
const getUserProfile = reactive(async (userId: number) => {
const user = await fetchQuery(GetUser, { id: userId });
const posts = await fetchQuery(GetUserPosts, { userId: user.id });
// Sequential fetch: posts depend on the user's ID from the first query
return { user, posts };
});
Without the transform, reactive functions are synchronous and return ReactivePromise objects. With the transform, you can write natural async/await code that Signalium converts into reactive computations behind the scenes. Dependencies are still tracked automatically, and the result updates when upstream data changes.
The async transform requires the Signalium Babel preset. See Babel Transform Setup below for configuration.
When to Use Which
Start with React hooks mode (useQuery). It is simpler, requires no build configuration, and covers the majority of use cases.
Consider Signalium mode when you need:
- Cross-component reactive composition --- multiple components sharing derived data from the same set of queries, without redundant computation or manual memoization.
- Derived queries --- queries whose parameters depend on the results of other queries (e.g., fetching a user's posts after fetching the user).
- Fine-grained render optimization --- large lists or complex UIs where property-level reactivity significantly reduces unnecessary re-renders.
- Shared reactive computations --- business logic that combines multiple data sources into a single reactive value, consumed by many parts of the UI.
You do not need to choose one mode for your entire application. It is common to use useQuery for simple data fetching in most components and reach for component() + fetchQuery() in performance-sensitive areas or where reactive composition simplifies the code.
Incremental Adoption
Adopting Signalium is incremental. You do not need to rewrite your application.
Step 1: Wrap components with component()
The simplest first step is wrapping existing components with component(). This gives you automatic memoization with zero other changes:
// Before
function UserList({ users }: { users: User[] }) {
return (
<ul>
{users.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
}
// After --- automatic memoization, no other changes needed
const UserList = component(({ users }: { users: User[] }) => {
return (
<ul>
{users.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
});
Step 2: Extract shared reactive functions
When you notice multiple components fetching the same data or computing the same derived values, extract a reactive() function:
// Shared across components
const getDashboardData = reactive((orgId: string) => {
const org = fetchQuery(GetOrg, { id: orgId });
const members = fetchQuery(GetMembers, { orgId });
const projects = fetchQuery(GetProjects, { orgId });
return { org, members, projects };
});
Step 3: Use async reactive functions for complex flows
For data flows where queries depend on each other, add the Babel transform and use async/await:
const getProjectDetails = reactive(async (projectId: string) => {
const project = await fetchQuery(GetProject, { id: projectId });
const owner = await fetchQuery(GetUser, { id: project.ownerId });
const tasks = await fetchQuery(GetTasks, { projectId });
return { project, owner, tasks };
});
Babel Transform Setup
The Signalium Babel transform is needed for async reactive functions. It rewrites async functions used with reactive() into generator-based coroutines that Signalium can pause and resume.
Installation
The transform is included in the signalium package. No additional dependencies are needed.
Configuration
Add the Signalium preset to your Babel configuration:
// babel.config.js
module.exports = {
presets: ['signalium/transform'],
};
If you are using other Babel presets (e.g., for React or TypeScript), add signalium/transform alongside them:
// babel.config.js
module.exports = {
presets: [
'@babel/preset-react',
'@babel/preset-typescript',
'signalium/transform',
],
};
What the transform does
The transform applies three rewrites:
- Async transform --- rewrites
asyncfunctions passed toreactive()into generators, enabling Signalium to track dependencies acrossawaitboundaries. - Callback transform --- wraps callback arguments in
callback()for reactive tracking inside event handlers and closures. - Promise methods transform --- replaces
Promise.all,Promise.race, and related methods withReactivePromiseequivalents, so concurrent data fetching integrates with the reactive system.
The transform only affects code that uses Signalium APIs (reactive, relay, task, etc.). It does not modify unrelated async functions or Promise usage. Standard async/await outside of reactive contexts is untouched.
Without the transform
If you prefer not to use a Babel transform, you can still use Signalium --- you just cannot use async/await inside reactive functions. Instead, access ReactivePromise values directly:
const getUserProfile = reactive((userId: number) => {
const userPromise = fetchQuery(GetUser, { id: userId });
const postsPromise = fetchQuery(GetUserPosts, { userId });
// Access .value on the reactive promise (returns undefined while loading)
const user = userPromise.value;
const posts = postsPromise.value;
if (!user || !posts) return undefined;
return { user, posts };
});
This approach works without any build tooling but requires manual handling of loading states.
Getting Started with Signalium
For a deeper understanding of Signalium's reactive programming model --- including signals, reactive functions, relays, watchers, and contexts --- see the Signalium documentation.
Key concepts to explore:
- Signals --- mutable state primitives (
signal()) - Reactive functions --- derived computations that automatically track dependencies (
reactive()) - Relays --- async computations with lifecycle management (
relay()) - Watchers --- side-effect subscriptions to reactive values (
watcher()) - Contexts --- dependency-injection-style scoping (
context(),getContext())
Fetchium's fetchQuery() returns a ReactivePromise (a Signalium async primitive), so understanding how Signalium handles async values will help you get the most out of the integration.