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, that means, no matter how many times you changed the value of a signal, its effects will only be executed once at the end of this tick. So if you modifred a signal's value and want to retrieve its updated derived signals value, you'll need to use nextTick(cb) or await nextTick() to get the new value. The lower-level tick() API exists to manually trigger a flush; prefer nextTick when you need to await the scheduler rather than calling tick() directly.
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 only when they are read during the synchronous execution of a computation. If you use early returns or branching logic, ensure all necessary signals are read before the branch:
const summary = computed(() => {
const t = track.value // read first to ensure tracking
const meta = metadata.value // read first to ensure tracking
if (!t) return 'Ready'
return `${t.name} – ${meta}`
})
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 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.