Skip to content

Function: useSignalEffect()

API / @xmachines/play-react / useSignalEffect

function useSignalEffect(callback, deps?): void;

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

The React hook that subscribes to the signal changes and runs an effect callback

The hook puts the callback in a Signal.Computed, which tracks each signal that the callback reads. It then gives the watcher lifecycle to watchSignal from @xmachines/play-signals. That function does the microtask batching, the new arm of the watcher, and the disposal. It is the one canonical implementation, and every framework renderer uses it.

Architecture:

  • The hook puts the callback in a Signal.Computed for the dependency tracking
  • watchSignal owns the Signal.subtle.Watcher lifecycle (Phase 29, memory safety)
  • The microtask batching groups rapid signal updates
  • The callback triggers each new render with its own setState (see the remarks)
  • The hook cleans up on unmount, so that nothing stays in memory

Invariant: Signal-Only Reactivity. The hook watches each signal that the callback reads. Invariant: Passive Infrastructure. React observes the signals, and it does not control them.

Parameters

ParameterTypeDefault valueDescription
callback() => void | (() => void)undefinedThe effect function that reads the signals. It can return a cleanup function.
depsDependencyList[]The optional dependency list, as in useEffect. On a change of one dependency, the hook removes the watcher and the Computed, then makes them again and tracks the signals from the start. The default is [], which subscribes one time for each mount. Give this list when the identity of the object whose signals you read can change during the life of the component, for example an actor prop. Without the list, the watcher tracks the signals of the OLD object for ever.

Returns

void

Example

const MyComponent = ({ actor }) => {
const [view, setView] = useState(null);
// Subscribe to the actor.currentView signal, and subscribe again on a new actor
useSignalEffect(() => {
const currentView = actor.currentView.get();
setView(currentView);
}, [actor]);
return <div>{view?.root}</div>;
};

Remarks

CRITICAL: read each signal unconditionally, with no if statement. A conditional read breaks the dependency tracking.

Performance: the microtask batching (queueMicrotask, inside watchSignal) stops a flood of React renders when several signals update in rapid sequence.

Re-rendering: the hook itself does NOT force a React render on a signal change. The callback triggers each new render, because it calls setState with the new signal value, as in the example above. The setState bailout of React can therefore skip a render when the derived value is the same. An unconditional force-update here caused one wasted render for each notification before, also when the setState of the callback made the bailout. If you must render again on every notification, and you read the signals during the render, copy the signal value into the React state inside the callback.

Implementation note: the hook puts the callback in a Signal.Computed, because Signal.subtle.Watcher cannot track an arbitrary function call. The Computed does the dependency tracking. watchSignal evaluates the Computed, which runs the callback, on each change of a tracked signal.