Skip to content

Function: useSignalEffect()

API / @xmachines/play-react / useSignalEffect

function useSignalEffect(callback, deps?): void;

Defined in: packages/play-react/src/useSignalEffect.ts:80

React hook that subscribes to signal changes and runs effect callback

Wraps the callback in a Signal.Computed to automatically track signal dependencies accessed inside it, then delegates the watcher lifecycle (microtask batching, re-arming, disposal) to watchSignal from @xmachines/play-signals — the single canonical implementation shared across framework renderers.

Architecture:

  • Uses Signal.Computed to wrap callback for automatic dependency tracking
  • watchSignal owns the Signal.subtle.Watcher lifecycle (Phase 29 memory safety)
  • Microtask batching coalesces rapid signal updates
  • Re-rendering is driven by the callback’s own setState (see remarks)
  • Handles cleanup on unmount to prevent memory leaks

Invariant: Signal-Only Reactivity - Signals accessed in callback are watched. Invariant: Passive Infrastructure - React observes signals and does not control them.

Parameters

ParameterTypeDefault valueDescription
callback() => void | (() => void)undefinedEffect function that accesses signals. Can return cleanup function.
depsDependencyList[]Optional dependency list (like useEffect). When any dependency changes, the watcher and Computed are torn down and recreated, re-tracking signals from scratch. Defaults to [] (subscribe once per mount). Pass this when the identity of the object whose signals you read can change over the component’s lifetime (e.g. an actor prop) — otherwise the watcher keeps tracking the OLD object’s signals forever.

Returns

void

Example

const MyComponent = ({ actor }) => {
const [view, setView] = useState(null);
// Subscribe to actor.currentView signal; re-subscribe if the actor swaps
useSignalEffect(() => {
const currentView = actor.currentView.get();
setView(currentView);
}, [actor]);
return <div>{view?.component}</div>;
};

Remarks

CRITICAL: Signals must be accessed unconditionally (no if statements). Conditional signal access breaks automatic dependency tracking.

Performance: Microtask batching (queueMicrotask, inside watchSignal) prevents React thrashing when multiple signals update rapidly.

Re-rendering: the hook itself does NOT force a React render on signal change — the callback drives re-rendering by calling setState with the new signal value (as in the example above). This lets React’s setState bailout skip renders when the derived value is unchanged; an unconditional force-update here previously caused a wasted render per notification even when the callback’s setState bailed. If you need to re-render on every notification while reading signals during render, mirror the signal value into React state inside the callback.

Implementation note: We wrap the callback in Signal.Computed because Signal.subtle.Watcher cannot automatically track arbitrary function calls. The Computed handles dependency tracking; watchSignal evaluates it (which runs the callback) whenever any tracked signal changes.