Effects
Effects let you run side effects in response to signal changes. They're essential for data fetching, DOM manipulation, and synchronization.
Basic Effects
import { signal, effect } from 'sigx';
const state = signal({ count: 0 });
// This runs immediately, then re-runs when count changes
effect(() => {
console.log('Count is:', state.count);
});
state.count = 5; // Logs: "Count is: 5"
Effects in Components
import { component, effect } from 'sigx';
type UserProfileProps = Define.Prop<'userId', number, true>;
const UserProfile = component<UserProfileProps>(({ signal, props }) => {
const state = signal({ user: null as any });
// Fetch user when userId changes
effect(() => {
fetch(`/api/users/${props.userId}`)
.then(r => r.json())
.then(data => state.user = data);
});
return () => (
<div>
{state.user ? (
<h1>{state.user.name}</h1>
) : (
<p>Loading...</p>
)}
</div>
);
});
Effect Cleanup
In a component: disposal is automatic
effect(), watch() and non-detached effectScope() created directly in a component's setup body are tied to that component's lifetime. They stop when it unmounts — and are re-created on HMR reload — so there is no handle to hold and nothing to call in onUnmounted:
const Search = component(() => {
const query = signal('');
// Stopped automatically when <Search /> unmounts.
watch(() => query.value, (q) => runSearch(q));
return () => <input onInput={(e) => (query.value = e.target.value)} />;
});
Only the setup body is scoped. A reaction created inside onMounted, onCreated, a setTimeout, or any async callback runs after setup has finished collecting, so it is not captured — dispose those yourself:
const Feed = component(() => {
onMounted(() => {
const runner = effect(() => console.log(count.value));
onUnmounted(() => runner.stop()); // ← still your job
});
return () => <div />;
});
computed() is lazy and holds no subscription of its own, so it never needs disposal.
Outside a component: stop it yourself
With no component setup to attach to — module scope, a plugin's install(), an event handler — the returned EffectRunner is the only thing that can stop the effect:
const runner = effect(() => {
const timer = setInterval(() => {
console.log('Tick');
}, 1000);
});
// Later: stop the effect
runner.stop();
Watch API
For more control over when callbacks fire, use watch:
import { signal, watch } from 'sigx';
const state = signal({ count: 0 });
// Watch a specific value
const stop = watch(
() => state.count,
(newValue, oldValue, onCleanup) => {
console.log(`Changed from ${oldValue} to ${newValue}`);
// Optional cleanup
onCleanup(() => {
console.log('Cleaning up...');
});
},
{ immediate: true } // Run immediately with current value
);
// Control the watcher
stop.pause(); // Pause watching
stop.resume(); // Resume watching
stop.stop(); // Stop completely
Watch Options
watch(source, callback, {
immediate: true, // Run callback immediately
deep: true, // Deep watch nested objects
once: true // Only trigger once
});
Batching Updates
Use batch to group multiple updates:
import { signal, batch, effect } from 'sigx';
const state = signal({ a: 1, b: 2 });
effect(() => {
console.log('Sum:', state.a + state.b);
});
// Without batch: logs twice
state.a = 10; // Logs
state.b = 20; // Logs
// With batch: logs once
batch(() => {
state.a = 100;
state.b = 200;
});
// Logs once: "Sum: 300"
Effect Scope
Group and manage multiple effects:
import { effectScope, effect } from 'sigx';
const scope = effectScope();
scope.run(() => {
effect(() => { /* tracked */ });
effect(() => { /* tracked */ });
});
// Dispose all effects at once
scope.stop();
A scope created in a component's setup body is itself scoped to that component, so it stops on unmount along with everything inside it. Pass effectScope(true) to detach it — a detached scope opts out of that capture and is yours to stop.
When to Use Effects
Good use cases:
- Data fetching
- DOM manipulation
- Event listeners
- Timers and intervals
- Syncing with external systems
Avoid:
- Computing derived values (use
computedinstead) - Modifying other signals (can cause infinite loops)
- Modifying signals within the same effect (can cause loops)
