Signals
Signals are the fundamental building blocks for rEFui. It is a lightweight, reactive signal system for building reactive applications. Signals provide a way to create reactive data that automatically updates dependent computations when the underlying data changes.
Core Concepts
Signals
Signals are reactive containers for values that can notify observers when they change. They form the foundation of the reactive system.
Effects
Effects are functions that automatically re-run when their dependencies (signals) change.
Computations
Computed signals derive their value from other signals and automatically update when dependencies change.
Important notice
Signal effects are semi-lazily computed: no matter how many times you change a signal during one tick, its effects execute once at the end of that tick. If you modify a signal and need its updated derived value, use nextTick(cb) or await nextTick(). The lower-level tick() API manually triggers a flush; prefer nextTick when you need to await the scheduler.
Derived signal propagation still flushes before user effects, but effects within the same phase should not communicate through assumed execution order. Coordinate related work through signals or nextTick() instead.
Avoiding Stale Values
Effects and computed signals flush at the end of the tick. If you need to read a fresh derived value immediately after a write, always use await nextTick().
Dependency Tracking
Signals track dependencies when they are read during the synchronous execution of a computation. Branches can discover dependencies incrementally: when a previously tracked condition changes and opens a branch, signals read in that branch become dependencies on that run.
const summary = computed(() => {
const t = track.value
if (!t) return 'Ready'
return `${t.name} – ${metadata.value}`
})
While track.value is falsy, metadata is not observed. Once track becomes truthy, the computation reruns, reaches metadata.value, and starts observing it. A dependency that is no longer read is retired lazily after its existing subscription next fires.
Basic Usage
Creating Signals
import { signal } from 'refui/signal'
// Create a signal with an initial value
const count = signal(0)
// Get the current value
console.log(count.value) // 0
// Update the value
count.value = 5
console.log(count.value) // 5
Creating Computed Signals
import { signal, computed, nextTick } from 'refui/signal'
const count = signal(0)
const doubled = computed(() => count.value * 2)
console.log(doubled.value) // 0
count.value = 5
nextTick(() => {
console.log(doubled.value) // 10
})
Effects
import { signal, watch } from 'refui/signal'
const count = signal(0)
// Watch for changes
const dispose = watch(() => {
console.log('Count changed:', count.value)
})
count.value = 1 // Logs: "Count changed: 1"
nextTick(() => {
count.value = 2 // Logs: "Count changed: 2"
})
// Clean up the effect
dispose()
Advanced Features
Custom Effects
const myEffect = () => {
const value = mySignal.value
console.log('Signal value:', value)
}
watch(myEffect)
Batched Updates
Updates are automatically batched and applied asynchronously:
count.value = 1
count.value = 2
count.value = 3
// Only triggers effects once with final value
Best Practices
-
Use computed signals for derived data:
const fullName = computed(() => `${first.value} ${last.value}`) -
Dispose of effects when no longer needed:
const dispose = watch(() => { // effect logic }) // Later... dispose() -
Use
peek()to avoid creating dependencies:const currentValue = mySignal.peek() // Doesn't create dependency -
Batch related updates:
// Updates are automatically batched firstName.value = 'John' lastName.value = 'Doe' // fullName updates only once -
Use
untrack()for non-reactive operations:const result = untrack(() => { // This won't create dependencies or inherit lifecycle ownership return someSignal.value + otherSignal.value })
Examples
Counter Example
import { signal, computed, watch } from 'refui/signal'
const count = signal(0)
const doubled = computed(() => count.value * 2)
watch(() => {
console.log(`Count: ${count.value}, Doubled: ${doubled.value}`)
})
count.value = 5 // Logs: "Count: 5, Doubled: 10"
Todo List Example
const todos = signal([])
const filter = signal('all')
const filteredTodos = computed(() => {
const todoList = todos.value
const currentFilter = filter.value
switch (currentFilter) {
case 'active':
return todoList.filter(todo => !todo.completed)
case 'completed':
return todoList.filter(todo => todo.completed)
default:
return todoList
}
})
// Add todo
function addTodo(text) {
todos.value = [...todos.value, { id: Date.now(), text, completed: false }]
}
// Toggle todo
function toggleTodo(id) {
todos.value = todos.value.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
}
For the full API documentation, see Signal API.