Skip to main content

Signals

Signals are the reactive primitive at the heart of Weblisk. They provide fine-grained reactivity — when a signal changes, only the code that depends on it re-runs. No virtual DOM, no diffing, no re-renders.

Creating a Signal

js
import { signal } from 'weblisk';

const [count, setCount] = signal(0);

// Read
console.log(count()); // 0

// Write
setCount(5);
console.log(count()); // 5

signal() returns a tuple: a getter function and a setter function.

Effects

An effect runs a function whenever its tracked signals change:

js
import { signal, effect } from 'weblisk';

const [name, setName] = signal('World');

effect(() => {
  document.title = `Hello, ${name()}!`;
});

setName('Weblisk'); // title updates to "Hello, Weblisk!"

Effects automatically track which signals were read during execution and re-run when any of them change.

Cleanup

effect() returns a dispose function:

js
const dispose = effect(() => {
  console.log(name());
});

dispose(); // Stop tracking, stop re-running

Computed Signals

A computed signal derives its value from other signals. It's lazy (only recalculated when read) and cached:

js
import { signal, computed } from 'weblisk';

const [price, setPrice] = signal(100);
const [tax, setTax] = signal(0.1);

const total = computed(() => price() * (1 + tax()));

console.log(total()); // 110
setTax(0.2);
console.log(total()); // 120

Store

A store is a reactive object — each property is a signal under the hood:

js
import { store, effect } from 'weblisk';

const user = store({ name: 'Alice', age: 30 });

effect(() => {
  console.log(`${user.name} is ${user.age}`);
});

user.name = 'Bob';   // Logs: "Bob is 30"
user.age = 31;       // Logs: "Bob is 31"

Stores use a Proxy so you can read/write properties with normal syntax.

Advanced

Peeking (Non-Tracking Read)

Read a signal without creating a dependency:

js
const [count, setCount] = signal(0);

effect(() => {
  // count.peek() doesn't track — this effect won't re-run when count changes
  console.log('Current count:', count.peek());
});

Subscribing

Listen to changes outside of effects:

js
const [count, setCount] = signal(0);

const unsub = count.subscribe((value) => {
  console.log('Changed to:', value);
});

setCount(1); // Logs: "Changed to: 1"
unsub();     // Stop listening

Patterns

Derived State

js
const [items, setItems] = signal([]);
const count = computed(() => items().length);
const empty = computed(() => items().length === 0);

Toggle

js
const [open, setOpen] = signal(false);
const toggle = () => setOpen(!open());

DOM Binding

js
enhance('[data-island="search"]', (el, { $, effect }) => {
  const [query, setQuery] = signal('');
  const input = $('input');
  const results = $('.results');

  input.addEventListener('input', (e) => setQuery(e.target.value));

  effect(() => {
    results.textContent = query() ? `Searching: ${query()}` : '';
  });
});