useEffect
useEffect(fn) runs fn after the DOM has been updated, and again whenever a signal or store property that fn read changes.
useEffect(fn) runs fn after the DOM has been updated, and again whenever a signal or store property that fn read changes. Dependencies are not declared: they are whatever fn read during its last run.
src/page-title.tsxtsx
import { useEffect, useStore } from "@shadow-js/core";
export function PageTitle() {
const [unread] = useStore(3);
useEffect(() => {
const previous = document.title;
document.title = `(${unread()}) Inbox`;
return () => {
document.title = previous;
};
});
return <h1>Inbox</h1>;
}
Semantics, each proven below:
- The first run is queued, not immediate. It happens at the next flush: when
render()returns (it callsflushSync()), at the end of the current event handler orbatch, atflushSync(), or in a microtask. - Dependencies are re-discovered on every run. A branch that was not taken last time is not tracked; a value read only in the first run stops being a dependency if it is not read again.
- A returned function is a cleanup. It runs before the next run and when the effect's owner is disposed. Any other return value is ignored.
fntakes no arguments in this form. There is no previous value; use the two-argument form for that.- The effect is owned by the current owner (a component, a
createRoot, a control-flow branch). Creating one with no owner warnsNO_OWNER_EFFECT, because nothing can ever dispose it. options.namelabels the effect in diagnostics.
The two-argument form: separate tracking from the side effect#
useEffect(deps, fn, options?) tracks only deps. fn(value, previous) runs untracked, with the value deps returned and the previous value (undefined the first time). It is how you react to one signal without subscribing to everything the effect body touches.