API reference

signalium/react

Functions

component

export default function component<Props extends object>(
  fn: (props: Props) => React.ReactNode | React.ReactNode[] | null,
): (props: Props) => React.ReactElement;

Create a reactive component from a pure function. Inside the function, read Signal values and other reactive sources directly. Re-renders are scheduled automatically when dependencies change.

import { component, useSignal } from 'signalium/react';
import { reactive, type Signal } from 'signalium';

const fullName = reactive(
  (first: Signal<string>, last: Signal<string>) =>
    `${first.value} ${last.value}`,
);

const Name = component(() => {
  const first = useSignal('Ada');
  const last = useSignal('Lovelace');

  return (
    <div>
      <p>{fullName(first, last)}</p>
      <button onClick={() => (first.value = 'Grace')}>First → Grace</button>
      <button onClick={() => (last.value = 'Hopper')}>Last → Hopper</button>
    </div>
  );
});
ParameterTypeDescription
fn(props: Props) => ReactNodeRender function

useSignal

export function useSignal<T>(
  value: T,
  opts?: {
    equals?: (prev: T, next: T) => boolean | false;
    id?: string;
    desc?: string;
  },
): Signal<T>;

Create a component-scoped Signal<T> for local state. The signal is stable across renders and disposed on unmount. Prefer useSignal over useState when you want granular reactivity and direct reads.

import { component, useSignal } from 'signalium/react';

const Counter = component(() => {
  const count = useSignal(0);

  return (
    <div>
      <button onClick={() => count.update((v) => v - 1)}>-</button>
      <span>{count.value}</span>
      <button onClick={() => count.update((v) => v + 1)}>+</button>
    </div>
  );
});

// Custom equality (always update)
const Search = component(() => {
  const query = useSignal('', { equals: () => false });
  return (
    <input
      value={query.value}
      onChange={(e) => (query.value = e.target.value)}
    />
  );
});

Signals can be passed as parameters to reactive functions, and the reactive function will update less often with signal parameters than with plain values. This is because reactive functions will rerun lazily, from innermost to outermost, and only rerun if the parameters or the signals they access have changed.

import { reactive, type Signal } from 'signalium';
import { component, useSignal } from 'signalium/react';

// Deeply nested reactive graph
const formatName = reactive((first: Signal<string>, last: Signal<string>) => {
  return `${first.value} ${last.value}`;
});

const greeting = reactive(
  (prefix: Signal<string>, first: Signal<string>, last: Signal<string>) => {
    return `${prefix.value} ${formatName(first, last)}`;
  },
);

const cardText = reactive(
  (
    title: Signal<string>,
    prefix: Signal<string>,
    first: Signal<string>,
    last: Signal<string>,
  ) => {
    return `[${title.value}] ${greeting(prefix, first, last)}`;
  },
);

const ProfileCard = component(() => {
  const title = useSignal('Engineer');
  const prefix = useSignal('Hello,');
  const first = useSignal('Ada');
  const last = useSignal('Lovelace');

  // Passing signals means only the minimal inner layers recompute
  const text = cardText(title, prefix, first, last);

  return (
    <div>
      <p>{text}</p>
      <button onClick={() => (first.value = 'Grace')}>First → Grace</button>
      <button onClick={() => (last.value = 'Hopper')}>Last → Hopper</button>
      <button onClick={() => (prefix.value = 'Hi,')}>Prefix → Hi,</button>
      <button onClick={() => (title.value = 'Captain')}>Title → Captain</button>
    </div>
  );
});

When first or last change, only formatName (and dependents) recompute. cardText does not rerun unless its own signal params (title, prefix, first, last) change or a nested reactive it calls produces a new value. Passing plain values instead of Signals would force outer layers to rerun more often.

ParameterTypeDescription
valueTInitial value
opts.equals((prev: T, next: T) => boolean) | falseEquality function
opts.idstringDebug identifier
opts.descstringDebug description

useReactive

export function useReactive<R>(signal: Signal<R>): R;
export function useReactive<R>(signal: ReactivePromise<R>): ReactivePromise<R>;
export function useReactive<R, Args extends readonly unknown[]>(
  fn: (...args: Args) => R,
  ...args: Args
): R;

Helper function to read reactive values in standard React components that are not defined with component. This hook is only needed if you are not converting a component to a reactive component, OR if you are using a reactive value inside of a custom hook that is also used in non-reactive components.

Examples:

import { useSignal, useReactive } from 'signalium/react';
import { reactive, task } from 'signalium';

// 1) Read a signal
const Display = () => {
  const message = useSignal('hello');
  const value = useReactive(message);
  return <p>{value}</p>;
};

// 2) Read a computed reactive function
// Here we create a reactive function and use it directly
const computeArea = reactive(
  (width: Signal<number>, height: Signal<number>) => width.value * height.value,
);

const Area = () => {
  const width = useSignal(3);
  const height = useSignal(4);
  const area = useReactive(computeArea, width, height);

  return (
    <div>
      <p>Area: {area}</p>
      <button onClick={() => (width.value += 1)}>W+1</button>
      <button onClick={() => (height.value += 1)}>H+1</button>
    </div>
  );
};

// 3) Read an async reactive function
const fetchUser = reactive(async (id: string) => {
  const res = await fetch(`/api/users/${id}`);
  return res.json() as Promise<{ id: string; name: string }>;
});

const User = () => {
  let user = useReactive(fetchUser, '1');

  return (
    <div>
      {user.isPending && <p>Loading…</p>}
      {user.error && <p>Error</p>}
      {user.isReady && <p>{user.value.name}</p>}
    </div>
  );
};
OverloadParametersReturns
Signalsignal: Signal<R>R
ReactivePromisesignal: ReactivePromise<R>ReactivePromise<R>
Functionfn: (...args) => R, ...argsR (or ReactivePromise if async)

useContext

export function useContext<T>(context: Context<T>): T;

Read a context value inside React components. Use with ContextProvider to supply values. Is cross-compatible between reactive and non-reactive components, but should still follow the rules of hooks inside reactive components.

import { component, useContext, ContextProvider } from 'signalium/react';
import { context } from 'signalium';

const Theme = context<'light' | 'dark'>('light');

const Label = component(() => {
  const theme = useContext(Theme);
  return <span>Theme: {theme}</span>;
});

const App = component(() => (
  <ContextProvider contexts={[[Theme, 'dark']]}>
    <Label />
  </ContextProvider>
));
ParameterTypeDescription
contextContext<T>Context to read

ContextProvider

export function ContextProvider(props: {
  contexts?: [...ContextPair<unknown[]>] | [];
  inherit?: boolean;
  children: React.ReactNode;
}): React.ReactElement;

Provide contexts to a React subtree using an array of context pairs. A context pair is a 2-tuple of [Context<T>, T]. This component is a flattened alternative to nesting many providers — pass multiple pairs in a single contexts array instead of creating deeply nested providers. Set inherit={false} to create an isolated scope that does not read parent contexts.

import { component, ContextProvider, useContext } from 'signalium/react';
import { context } from 'signalium';

const Theme = context<'light' | 'dark'>('light');
const Lang = context<'en' | 'es'>('en');

const Read = component(() => {
  const theme = useContext(Theme);
  const lang = useContext(Lang);
  return (
    <p>
      {lang} / {theme}
    </p>
  );
});

const App = component(() => (
  <ContextProvider
    contexts={[
      [Theme, 'dark'],
      [Lang, 'es'],
    ]}
  >
    {/* Both Theme and Lang are provided without nesting */}
    <Read />

    {/* Override Lang only for a subtree */}
    <ContextProvider contexts={[[Lang, 'en']]}>
      <Read />
    </ContextProvider>

    {/* Create an isolated scope that ignores parents */}
    <ContextProvider
      inherit={false}
      contexts={[
        [Theme, 'light'],
        [Lang, 'en'],
      ]}
    >
      <Read />
    </ContextProvider>
  </ContextProvider>
));
PropTypeDescription
contexts[...ContextPair<unknown[]>]Contexts to provide
inheritbooleanInherit parent scope (default true)
childrenReact.ReactNodeChildren

SuspendSignalsProvider

export function SuspendSignalsProvider(props: {
  value: boolean;
  children: React.ReactNode;
}): React.ReactElement;

Temporarily suspend signal subscriptions for an entire React subtree. When value={true}, components in the subtree will not subscribe to signal updates, preventing re-renders and allowing signals to be garbage collected if not used elsewhere. When value={false}, components resume normal signal subscription.

This is particularly useful for React Native applications where screens remain mounted but inactive (e.g., background tabs), or when you need to temporarily pause expensive computations for performance reasons.

import { component, SuspendSignalsProvider, useSignal } from 'signalium/react';
import { reactive } from 'signalium';

const expensiveComputation = reactive((input: Signal<number>) => {
  // Heavy computation
  return input.value * Math.random();
});

const TabNavigator = component(() => {
  const [activeTab, setActiveTab] = useState('home');
  const data = useSignal(0);

  return (
    <>
      {/* Home tab - active when selected */}
      <SuspendSignalsProvider value={activeTab !== 'home'}>
        <div style={{ display: activeTab === 'home' ? 'block' : 'none' }}>
          <HomeTab data={data} />
        </div>
      </SuspendSignalsProvider>

      {/* Profile tab - suspended when not selected */}
      <SuspendSignalsProvider value={activeTab !== 'profile'}>
        <div style={{ display: activeTab === 'profile' ? 'block' : 'none' }}>
          <ProfileTab data={data} />
        </div>
      </SuspendSignalsProvider>

      <button onClick={() => setActiveTab('home')}>Home</button>
      <button onClick={() => setActiveTab('profile')}>Profile</button>
    </>
  );
});

// React Native example with navigation
import { useIsFocused } from '@react-navigation/native';

const TabScreen = component(() => {
  const isFocused = useIsFocused();

  return (
    <SuspendSignalsProvider value={!isFocused}>
      <YourTabContent />
    </SuspendSignalsProvider>
  );
});

Behavior:

  • Suspended (value={true}): Components don't subscribe to signals, updates don't trigger re-renders, last known values are retained, signals may be garbage collected
  • Active (value={false}): Normal signal subscription, updates trigger re-renders, signals show current values

Important notes:

  • Suspended signals will still rerun if the component tree re-renders for other reasons (e.g., prop changes), but they will compute with the last known values for any Relays in the suspended subtree
  • For permanent cleanup, unmount the component normally — suspension is for temporary pauses
  • Works with both useReactive and component() which are the primary signal entry points in React
PropTypeDescription
valuebooleanWhether to suspend (true) or not
childrenReact.ReactNodeChildren to suspend/resume
Previous
signalium