Quick Recap
This article continues from the conclusion of the previous one.
We will look at practical examples to understand how React and signals can complement each other in real applications.
Effect Cleanup Strategies
First, let’s clarify the core concept:
Our effect (
createEffect) is responsible for data/business cycles. It depends on signals and releases resources using onCleanup before re-running.React effects (
useEffect/useLayoutEffect) are responsible for UI/DOM cycles. They depend on React snapshots (props/state) and clean up before the next commit.
Practical Examples
Timer Example (using createEffect)
Requirement
Poll data based on an adjustable intervalMs and write the latest time into a heartbeat signal.
Key idea
When intervalMs changes, the old setInterval should be automatically cleared and replaced with a new one.
// data/heartbeat.ts
import { signal } from "../core/signal";
import { createEffect, onCleanup } from "../core/effect";
export const intervalMs = signal(1000);
export const heartbeat = signal<Date | null>(null);
createEffect(() => {
const ms = intervalMs.get(); // depend on signal
const id = setInterval(() => {
heartbeat.set(new Date()); // write back to the data layer
}, ms);
onCleanup(() => clearInterval(id)); // release before next re-run
});
Why place this inside createEffect?
This is purely a data-layer task:
- it depends on signals and writes back to signals.
- It does not interact with the DOM and does not require React’s commit timing.
UI Blinking Example (using useEffect)
Requirement
Make a cursor blink every 500ms.
Key idea
Since this behavior is tightly coupled to the DOM/rendering, it should be managed by React’s lifecycle.
// ui/Blinker.tsx
import { useState, useEffect } from "react";
export function Blinker({ enabled = true }) {
const [on, setOn] = useState(false);
useEffect(() => {
if (!enabled) return;
const id = setInterval(() => setOn(v => !v), 500);
return () => clearInterval(id); // cleanup before next commit
}, [enabled]);
return <span className={on ? "caret on" : "caret"}>|</span>;
}
Why place this inside useEffect?
This is purely a UI/visual behavior:
it depends on React state/props, and its cleanup timing should follow React’s commit cycle.
Using Them Together Without Interference
// App.tsx
import { useSignalValue } from "./react-adapter";
import { heartbeat, intervalMs } from "../data/heartbeat";
import { Blinker } from "./ui/Blinker";
export function Dashboard() {
const lastBeat = useSignalValue(heartbeat);
const ms = useSignalValue(intervalMs);
return (
<section>
<h3>Last heartbeat: {lastBeat?.toLocaleTimeString() ?? "—"}</h3>
<p>Polling every {ms} ms</p>
<Blinker enabled /> {/* UI timer managed by React */}
</section>
);
}
Timer polling (createEffect) exists independently of any component and continues even across page navigation.
UI blinking (React useEffect) is created and cleaned up along with component mount/unmount.
The cleanup timing does not conflict:
-
createEffect→onCleanupruns before re-execution - React
useEffect→ cleanup runs before the next commit
Common Mistakes and Fixes
Mistake: Using createEffect to control DOM timers
createEffect(() => {
const id = setInterval(() => {
el.classList.toggle("on"); // DOM timing not controlled by React
}, 500);
onCleanup(() => clearInterval(id));
});
Fix: Let React manage it with useEffect
(See the Blinker example above)
Mistake: Using React useEffect for data polling and directly using a signal as dependency
// Only runs once on mount, will not rebuild when interval changes
React.useEffect(() => {
const id = setInterval(fetchData, ms /* passing signal directly */);
return () => clearInterval(id);
}, []); // empty deps
Fix: Two possible approaches
A) Use the hook we created to obtain a snapshot
const ms = useSignalValue(intervalMs);
React.useEffect(() => {
const id = setInterval(fetchData, ms);
return () => clearInterval(id);
}, [ms]);
B) Move it back to the data layer with createEffect
Do not call signal functions directly inside React components.
Keep signals outside the component closure to maintain a clean separation.
When you need to read signals inside React, use hooks to access them.
Quick Concept Summary
| Scenario | Recommended Location | Dependency Source | Cleanup Timing |
|---|---|---|---|
| Data polling, cache updates, writing to signals | our effect | signal.get() | onCleanup before rerun |
| Visual effects, DOM operations, measurements | React effect | React state/props | cleanup before next commit |
| Batching multiple synchronous set() calls | scheduler (built-in microtask) | — | microtask flush |
| Read current value without reverse dependency | useSignalValue / peek() | not tracked | — |
Understanding Through Mental Models
Responsibilities of Each Layer
Cleanup Order (during a single update)
FAQ
Q: Why does getSnapshot use peek() instead of get()?
A:
To avoid adding the React component itself to the reactive graph’ssubs.
peek()will still perform lazy recomputation when the signal is stale, so the snapshot remains up to date.
Q: Can I call createEffect inside the render phase (inside a function component)?
A:
It’s best not to.
The adapter should create and clean up subscriptions throughsubscribeReadablewithin theuseSyncExternalStorelifecycle.
Q: Do I need to manually batch multiple synchronous set() calls?
A:
No. Our scheduler already merges updates within the same tick into a single microtask.
Cross-awaitbatching will be discussed in the advanced section (transaction(async)).
Conclusion
With the examples above, it should be clearer how signals can complement React.
From a usage perspective, the experience may feel similar to libraries like Jotai or Zustand, but signals provide finer-grained control, are not tied to a specific framework, and therefore offer higher compatibility across environments.
In the next article, we will cover practical patterns and checklists for handling:
- key-based remounting
- stale closures
- consistency of values read during Transitions
With concrete patterns you can directly apply.


Top comments (0)