Signals
Signals are EmberKit's reactive primitives. They store state and notify subscribers when values change — without re-rendering the entire page.
Creating a Signal
import { createSignal } from '@emberkit/core';
const [count, setCount] = createSignal(0);
// Read the value
console.log(count()); // 0
// Update the value
setCount(1);
console.log(count()); // 1
A signal returns a tuple: a getter function and a setter function.
Updating Signals
// Set a static value
setCount(5);
// Set based on previous value (function updater syntax)
setCount((prev) => prev + 1);
Signal Options
const [name, setName] = createSignal('Alice', {
equals: (prev, next) => prev === next, // custom equality check
});
Subscribing to Changes
Every signal has a subscribe method that registers a callback, fired whenever the value changes. The callback receives the new value.
const [count, setCount] = createSignal(0);
const unsub = count.subscribe((newVal) => {
console.log('count changed to', newVal);
});
setCount(5); // logs "count changed to 5"
// Clean up subscription when done
unsub();
How it works
Hydration uses subscribe to update bound DOM nodes. See Hydration.
Computed Values with createMemo
createMemo derives a value from other signals. It only recalculates when dependencies change:
import { createSignal, createMemo } from '@emberkit/core';
const [count, setCount] = createSignal(0);
const doubled = createMemo(() => count() * 2);
console.log(doubled.value); // 0
setCount(3);
console.log(doubled.value); // 6
Effects with createEffect
0.8.0+
createEffect and createMemo now track signal dependencies and re-run when those signals change (with batch / untrack support). Effects do not run during SSR.
Effects run side effects when signals change:
import { createSignal, createEffect } from '@emberkit/core';
const [theme, setTheme] = createSignal('light');
createEffect(() => {
document.body.className = theme();
});
// Later: triggers the effect
setTheme('dark');
Effects can return a cleanup function:
createEffect(() => {
const handler = () => console.log('resize');
window.addEventListener('resize', handler);
return () => {
window.removeEventListener('resize', handler);
};
});
Batch Updates
batch groups multiple signal updates into a single notification:
import { createSignal, batch } from '@emberkit/core';
const [firstName, setFirstName] = createSignal('John');
const [lastName, setLastName] = createSignal('Doe');
batch(() => {
setFirstName('Jane');
setLastName('Smith');
});
// Only one notification sent
Untracking
untrack reads a signal without tracking it as a dependency:
const [count, setCount] = createSignal(0);
createEffect(() => {
console.log(count());
untrack(() => {
console.log(count());
});
});
DOM binding (signals → DOM)
Signals pair with data-ek-bind so updates hit specific nodes instead of re-rendering the tree:
const [count, setCount] = createSignal(0);
return <span data-ek-bind={count}>{count()}</span>;
All binding attributes (data-ek-show, data-ek-hide, data-ek-show-when, …) are documented in Hydration.
Components accepting signals
Components may detect signal props and wire bindings internally. Pass a signal or a plain boolean for SSR-only output:
<Modal open={open} onClose={() => setOpen(false)} />
<Modal open={true} />
See Components.
Complete Example
import { createSignal, createMemo, createEffect } from '@emberkit/core';
function Counter() {
const [count, setCount] = createSignal(0);
const doubled = createMemo(() => count() * 2);
const isEven = createMemo(() => count() % 2 === 0);
createEffect(() => {
document.title = `Count: ${count()}`;
});
return (
<div>
<p>Count: {count()}</p>
<p>Doubled: {doubled.value}</p>
<p>{isEven.value ? 'Even' : 'Odd'}</p>
<button type="button" onClick={() => setCount((c) => c + 1)}>Increment</button>
<button type="button" onClick={() => setCount(0)}>Reset</button>
<span data-ek-bind={count}>{count()}</span>
</div>
);
}
Next Steps
- Hydration —
data-ek-bindreference - Context — Share signals across components
- Components — Static vs interactive components
- UI Components — Modal, Tabs, Counter demos