The Lifecycle Never Went Away
From the beginning of this series up to the current implementation, everything has revolved around the lifecycle of the data layer:
How data is read, invalidated, recomputed, and when side effects are triggered.
This does not conflict with the framework’s lifecycle. In fact, React never removed lifecycle — it restructured it into two distinct phases:
- Render: Purely computes UI. It may run multiple times, be interrupted, or discarded. Ideally, no side effects should occur here.
-
Commit: Applies changes to the DOM in a single, synchronous step.
useLayoutEffect/useEffectsetup and cleanup run here. This is the only legitimate place for UI side effects.
If you’re not familiar with useLayoutEffect and useEffect, review how React manages mount/unmount timing via these hooks. That understanding is foundational.
React emphasizes that in typical usage, UI dependencies on state are not explicitly declared. When state updates, React re-runs the component render and uses VDOM diffing to determine minimal DOM changes. Whether child subtrees update depends on bailout and memoization strategies.
Signals take a different route: an explicit dependency graph.
The system knows who depends on whom, allowing precise update propagation. A scheduler (e.g., microtask batching) controls when side effects run.
The two approaches may look different, but both rigorously manage lifecycle boundaries:
- React protects side-effect timing via the Render/Commit boundary.
- Signals manage data lifecycle through invalidation, recomputation, and subscription boundaries.
The lifecycle never disappeared — it simply exists at different abstraction layers.
Goal of This Article
We clearly separate responsibilities:
-
UI-related side effects (DOM measurement, manipulation, animation) → React (
useLayoutEffect/useEffect) -
Data-flow side effects (business logic triggered by signal/computed changes) → signals (
createEffect, lifecycle managed by our adapter) -
Render must remain pure — no
signal.set()or external effect creation during render.
Timing Overview: Who Runs First? Who Cleans First?

React effect cleanup (useEffect / useLayoutEffect) runs before the next commit.
Our signal effect cleanup (onCleanup) runs before re-execution within the same microtask batch.
They operate independently and do not conflict.
Responsibility Separation
| Concern | Where It Belongs | Explanation |
|---|---|---|
| Read/write DOM, measurement, animation |
useLayoutEffect / useEffect
|
Runs after commit, timing is predictable |
| Trigger business logic (requests, logging, cross-layer events) |
createEffect (via adapter subscription) |
Scheduler batches and re-runs in microtasks |
Merge multiple synchronous set() calls |
Scheduler (microtask dedupe) | Effects re-run only once per batch |
| Read current snapshot |
useSignalValue / useComputed
|
useSyncExternalStore + peek() prevents tearing in Concurrent mode |
Correct Usage Patterns
1. Writing During Render vs Writing in Event / Effect
❌ Incorrect (side effect during render)
function Bad() {
const v = useSignalValue(mySig);
if (v < 0) mySig.set(0); // Writing during render → infinite re-render / StrictMode issues
return null;
}
✅ Correct (write in React effect)
function Good() {
const v = useSignalValue(mySig);
React.useEffect(() => {
if (v < 0) mySig.set(0);
}, [v]); // Safe: runs after commit
return null;
}
Or inside an event handler:
<button onClick={() => mySig.set(x => Math.max(0, x))}>
Clamp
</button>
This mirrors a common React rule: never mutate state during function component render.
2. DOM Manipulation in Our Effect vs React Effect
❌ Incorrect (DOM mutation inside our effect)
createEffect(() => {
const h = panelHeight.get();
panelEl.style.height = h + "px"; // May conflict with React commit
});
✅ Correct (pass value to React; mutate DOM in layout effect)
function Panel({ el }: { el: HTMLElement }) {
const h = useSignalValue(panelHeightSignal);
React.useLayoutEffect(() => {
el.style.height = h + "px"; // Safe: runs after commit
}, [el, h]);
return null;
}
The DOM lifecycle belongs to React.
Our effects are better suited for data-level side effects.
3. useEffect Subscription vs useSyncExternalStore
❌ Incorrect (manual subscription via useEffect → tearing risk)
function BadSubscribe() {
const [v, setV] = React.useState(mySig.peek());
React.useEffect(() => {
const stop = createEffect(() => {
setV(mySig.peek()); // May cause tearing
});
return () => stop();
}, []);
return <div>{v}</div>;
}
✅ Correct (use adapter hook built on useSyncExternalStore)
function GoodSubscribe() {
const v = useSignalValue(mySig); // useSyncExternalStore + peek()
return <div>{v}</div>;
}
useSyncExternalStore guarantees snapshot consistency under Concurrent rendering, preventing tearing.
(For older React versions, a third-party shim can be considered.)
4. Computed Depending on React Snapshot vs Signal
❌ Incorrect (computed depends on React value)
const count = useSignalValue(countSig);
const doubled = useComputed(() => count * 2); // Only runs once
This does not establish a dependency in the reactive graph.
✅ Correct (computed reads signal directly)
const count = useSignalValue(countSig);
const doubled = useComputed(() => countSig.get() * 2); // Dependency tracked
Or use pure React derivation when no reactive node is needed:
const count = useSignalValue(countSig);
const doubled = useMemo(() => count * 2, [count]);
A computed must call .get() inside its execution to be tracked.
Lessons from These Mistakes
From the examples above, we can extract clear principles:
- Render must remain pure — no
createEffectorsignal.set()during render. - UI side effects →
useLayoutEffect/useEffect. - Data side effects →
createEffect. - Always subscribe via
useSignalValue(useSyncExternalStore). -
computedmust read.get(), not React snapshots.
Conclusion
React Effects include UI rendering updates.
If we want our signal system to work correctly within React, integration must respect React’s lifecycle hooks.
In short:
- APIs from our mechanism should accept signals directly.
- Values wrapped via hooks take on React’s shape, and therefore must interact through React hooks.
Only then will dependency tracking behave correctly.
In the next article, we’ll look at concrete examples demonstrating how both systems can complement each other.
Top comments (0)