Function: useAtomEffect()
API / @xmachines/play-react / useAtomEffect
function useAtomEffect(sources, callback, deps?): void;Defined in: packages/play-react/src/useAtomEffect.ts:106
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
sourcesarray 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
| Parameter | Type | Default value | Description |
|---|---|---|---|
sources | readonly Readable<unknown>[] | undefined | The 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) | undefined | The effect function. It can return a cleanup function. It runs one time on the mount, and one time for each write of a source. |
deps | DependencyList | [] | 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.