Why Does the Framework Affect How You Use Signals?
The short answer is lifecycle.
On social platforms, you’ll often see a group of React developers pretending lifecycle doesn’t exist while selling courses. That honestly worries me a bit—but once you go deep enough, you’ll realize this is mostly marketing rhetoric.
After implementing our signal system step by step in previous articles, you should already have a solid intuition for what lifecycle actually means. In this article, we’ll clarify three key points:
1. Two-phase mental model
React has Render and Commit.
Render can run multiple times and be interrupted; only Commit mutates the DOM.
2. Timing alignment
Our signals merge side effects in microtasks via a scheduler.
React uses event batching and Concurrent rendering.
3. Forbidden zones and correct placement
What must not happen during React’s render phase (e.g. createEffect, signal.set)—and where these belong instead.
TL;DR
- In React, render must be pure (the Function Component contract). Do not perform side effects or write to signals during render.
-
Side-effect responsibility split:
- UI-related (DOM measurement, animations) →
useLayoutEffect/useEffect - Data-flow related (business logic triggered by signals, requests) →
createEffect(managed via a React adapter)
- UI-related (DOM measurement, animations) →
-
Subscriptions must go through
useSyncExternalStore(covered next article) to avoid tearing.
React vs Signals: Timing Differences
- React render may run many times (Concurrent mode, StrictMode).
Commit runs only once. - Our scheduler merges effects at the microtask boundary.
- Therefore: never create or trigger
createEffectduring render.
Let hooks manage lifecycle instead.
StrictMode and Concurrent Traps
StrictMode (development only)
React will:
create → immediately clean up → create again
This is intentional—to detect side effects during render.
Concurrent Rendering
- Render can be interrupted and restarted.
- Reading external mutable state during render can cause tearing.
Official recommendation:
Use useSyncExternalStore to provide snapshots + subscriptions.
React can re-read the snapshot before commit, preventing tearing.
What Not to Do
Creating createEffect during render
function Bad() {
// Creating external effects during render breaks purity and predictability
createEffect(() => {
console.log("value", someSignal.get());
});
return <>{/* ...your UI */}</>;
}
Writing to signals during render
function Bad() {
const v = someSignal.get();
if (v < 0) someSignal.set(0); // Writing during render → infinite re-renders
return <>{/* ...your UI */}</>;
}
Capturing get() values in long-lived closures
function Bad() {
const v = someSignal.get(); // snapshot at render time
const onClick = () => console.log(v); // always logs stale value
return <button onClick={onClick}>log</button>;
}
Correct Patterns
Subscriptions
- Wrap signals with
useSyncExternalStore→useSignalValue(src) - Use
peek()for snapshots:- No React → signal dependency
- Lazy recompute when stale
- Inside
subscribe, usecreateEffectto track changes - Clean up effects on unmount
Writes
- Perform writes in event handlers or React effects
- Never during render
DOM Side Effects
- Still go through
useLayoutEffect/useEffect - Pass signal values into React
- Let React control DOM timing
We won’t implement this yet—this section is to establish the mental model first.
React Batching vs Our batch
React batches setState calls inside event handlers.
Our batch:
- Only affects signal effect scheduling
- Does not interfere with React’s commit phase
Example
batch(() => {
a.set(10);
b.set(20);
a.set(30);
});
// Our effects rerun once in a microtask
// React setState (if any) still batches render once per event
Side-Effect Responsibility Table
| Task | Where to Put It | Why |
|---|---|---|
| DOM reads/writes, measurement, animation |
useLayoutEffect / useEffect
|
Controlled by React’s Commit phase |
| Business effects triggered by signals |
createEffect (via hooks) |
Scheduler merges reruns |
| Multiple updates, single rerun |
batch / transaction
|
Affects signals only |
| React subscribing to external state | useSyncExternalStore |
Prevents tearing, supports Concurrent |
| Immediate effect flush (tests/demos) |
flushSync() (ours) |
Clears scheduler immediately |
Incorrect vs Correct
❌ Wrong: Subscribing + updating React state manually
function Bad() {
const [state, setState] = useState();
const v = someSignal.get();
useEffect(() => {
// Prone to tearing and duplicate updates
const stop = createEffect(() => {
someSignal.get();
setState(Date.now());
});
return () => stop();
}, []);
return <div>{v}</div>; // v is not a React-managed snapshot
}
✅ Correct: Use useSyncExternalStore
function Good() {
// Implemented in the next article
const v = useSignalValue(someSignal);
return <div>{v}</div>; // React-controlled synchronous snapshot
}
Conclusion
With these concepts in place, you should now have a more accurate understanding of React’s rendering and update mechanics.
Our goal is not to replace React’s state system.
Instead, we treat signals as external data sources—integrated in a way that:
- avoids tearing in Concurrent mode
- minimizes unnecessary reruns
- keeps behavior predictable
In the next article, we’ll implement the missing pieces and show how to correctly integrate our signal system into React.


