Skip to content

Function: useAtomEffect()

API / @xmachines/play-react / useAtomEffect

function useAtomEffect(sources, callback, deps?): void;

Defined in: packages/play-react/src/useAtomEffect.ts:105

The React hook that subscribes to the named atoms and runs an effect callback

The hook puts the SOURCES in a computed atom, and it gives the subscription lifecycle to watchAtom from @xmachines/play-atom. That function owns the coalescing and the disposal. It is the one canonical implementation, and every framework renderer uses it.

Architecture:

  • The sources array names every atom that runs the effect
  • The callback runs OUTSIDE the tracking scope, so a read inside it adds no dependency
  • watchAtom owns the subscription lifecycle
  • 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: Atom-Only Reactivity. The hook watches each atom of sources. Invariant: Passive Infrastructure. React observes the atoms, and it does not control them.

Parameters

ParameterTypeDefault valueDescription
sourcesreadonly Readable<unknown>[]undefinedThe atoms that run the effect. The list names every dependency, and the reads of the callback name none: a callback that reads a view store therefore depends on that store not, and a write of the store runs the effect never.
callback() => void | (() => void)undefinedThe effect function. It can return a cleanup function. It runs one time on the mount, and one time for each write of a source.
depsDependencyList[]The optional dependency list, as in useEffect. On a change of one dependency, the hook removes the watcher and the computed atom, then makes them again and reads the sources from the start. The default is [], which subscribes one time for each mount. Give this list when the identity of the object whose atoms you read can change during the life of the component, for example an actor prop. Without the list, the watcher tracks the atoms of the OLD object for ever.

Returns

void

Example

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

Remarks

CRITICAL: name every atom that must run the effect in sources. A read inside the callback tracks nothing, so an atom that the list leaves out runs the effect never.

How often the callback runs: one time for each MICROTASK, and not one time for each write. watchAtom owns that rule: it keeps the synchronous notification of @xstate/store and it defers the callback, so one batch of synchronous writes reaches the callback one time, with the last value. Three writes in one tick therefore run the callback ONE time.

The DERIVATION is the part that runs for each write: @xstate/store evaluates a computed atom synchronously inside the set of a dependency, and that evaluation IS the dependency tracking. Keep each source atom cheap to derive.

Re-rendering: the hook itself does NOT force a React render on an atom change. The callback triggers each new render, because it calls setState with the new atom value, as in the example above. The setState bailout of React can skip a render when the derived value is the same.

A callback that throws: the FIRST run happens while the hook mounts, so a throw there reaches the caller and React reports it. A later run happens in the microtask of watchAtom, and a throw there reaches no caller. The hook catches it and throws it again from a microtask of its own, which reaches the global handler of the runtime.

A SOURCE that throws: a source derives its value inside the set of the atom that it reads, and @xstate/store evaluates it there for every watcher. A throw from that derivation therefore leaves the set of the WRITER: the write fails, and every other observer of that write is skipped. The hook can catch none of it, because the engine runs the derivation before it reaches this hook — a watched computed atom that throws does the same with no hook anywhere.

Therefore a source derivation must return a value and throw nothing. currentView and currentRoute of @xmachines/play-xstate follow that rule: each one catches its own failure, reports it through onError of the actor from a microtask, and keeps the last value. Write a source of your own the same way.

Implementation note: the hook puts the SOURCES in a computed atom, because a subscription needs one atom to watch. That computed atom reads each source and holds the dependency edges, watchAtom owns the subscription that keeps it live, and the callback runs from the observer, which @xstate/store calls with no active subscriber.