Signals
Signals are the foundation of Effuse's reactivity system. They are imported directly from @effuse/core.
Creating Signals
Import signal from @effuse/core to create reactive state:
import { define, signal, computed } from '@effuse/core';
export const Counter = define({
script: () => {
// Create a signal with initial value
const count = signal(0);
// Create computed derived state
const doubleCount = computed(() => count.value * 2);
// Define operations that mutate the signal
const increment = () => count.value++;
const decrement = () => count.value--;
// Return signals and operations to template
return { count, doubleCount, increment, decrement };
},
template: ({ count, doubleCount, increment, decrement }) => (
<div>
<p>Count: {count}</p>
<p>Double: {doubleCount}</p>
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
</div>
),
});
Reactivity Types
1. Writable Signals
The basic signal() creates a writable reference. Access and mutate via the .value property.
import { define, signal } from '@effuse/core';
const ColorPicker = define({
script: () => {
// Primitives
const color = signal('blue');
// Objects/Arrays
const palette = signal(['red', 'blue', 'green']);
const updateColor = (newColor: string) => {
color.value = newColor; // Triggers updates
};
return { color, palette, updateColor };
},
template: ({ color, updateColor }) => (
<button onClick={() => updateColor('red')}>Current: {color}</button>
),
});
2. Computed Signals
Computed signals derive their value from other signals. They update automatically when dependencies change.
import { define, signal, computed } from '@effuse/core';
const GradientBox = define({
script: () => {
const startColor = signal('red');
const endColor = signal('blue');
// Automatically tracks dependencies
const gradient = computed(
() => `linear-gradient(${startColor.value}, ${endColor.value})`
);
return { gradient };
},
template: ({ gradient }) => (
<div style={`background: ${gradient.value}`}>Gradient</div>
),
});
3. Watching Signals
Use the watch helper from the script context to perform side effects when a signal changes.
import { define, signal } from '@effuse/core';
const Logger = define({
script: ({ watch }) => {
const count = signal(0);
// Runs whenever count changes
watch(count, (value) => {
console.log(`Count changed to: ${value}`);
});
return { count, increment: () => count.value++ };
},
template: ({ count, increment }) => (
<button onClick={increment}>{count}</button>
),
});
Advanced Watch Options
The watch function accepts an optional EffectOptions object:
watch(
count,
(value) => {
console.log('Count changed');
},
{
debounce: { wait: 300 },
timeout: 5000,
retry: {
times: 3,
delay: 1000,
},
flush: 'post',
}
);
Watching Multiple Signals
Use watchMultiple to react to changes in any of the provided signals:
import { watchMultiple } from '@effuse/core';
watchMultiple([signalA, signalB], ([valA, valB]) => {
console.log('Either A or B changed', valA, valB);
});
Specialized Signals
Readonly Signals
Create a readonly view of a signal to prevent external mutations:
import { signal, readonlySignal } from '@effuse/core';
const count = signal(0);
const readonlyCount = readonlySignal(count);
// readonlyCount.value++ -> Error in development
Writable Computed
Computed signals are naturally readonly, but you can create writable computed signals by providing a setter:
const fullName = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (newValue) => {
const [first, last] = newValue.split(' ');
firstName.value = first;
lastName.value = last;
},
});
Using Signals in Templates
Signals can be used directly in JSX - they will automatically update the DOM:
// Direct interpolation - updates automatically
<p>Count: {count}</p>
// Dynamic classes with functions
<button class={() => isActive.value ? 'active' : 'inactive'}>
Toggle
</button>
// Conditional rendering with computed
{computed(() => isLoading.value ? <Spinner /> : <Content />)}
Best Practices
Import Directly: Import
signalandcomputeddirectly from@effuse/coreExpose Signals: Return the signal object itself from
script, not just the valueMutate in Handlers: Keep state mutation logic inside function handlers
Use Computed When Needed: Prefer derived state over manual synchronization
Compiler Optimizations
The Effuse compiler automatically handles many reactivity scenarios:
Auto-Wrapping in Templates
When you use expressions in templates, the compiler automatically wraps them in computed signals. You don't need to manually wrap every derived value:
// The compiler handles this automatically
template: ({ firstName, lastName }) => (
<p>
Full Name: {firstName} {lastName}
</p>
);
// No need for explicit computed() in simple cases
// The compiler will optimize the binding
When to Use Explicit computed()
Use explicit computed() when:
Complex derived state - Multiple signals combined with logic
Returned from script - For values exposed to multiple consumers
Performance optimization - To cache expensive calculations
script: () => {
const items = signal<Item[]>([]);
const filter = signal('all');
// Explicit computed for complex logic returned to template
const filteredItems = computed(() => {
const f = filter.value;
return items.value.filter((item) =>
f === 'all' ? true : item.status === f
);
});
return { filteredItems, filter };
};
Next Steps