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

  1. Use computed signals for derived data:

    const fullName = computed(() => `${first.value} ${last.value}`)
    
  2. Dispose of effects when no longer needed:

    const dispose = watch(() => {
        // effect logic
    })
    
    // Later...
    dispose()
    
  3. Use peek() to avoid creating dependencies:

    const currentValue = mySignal.peek() // Doesn't create dependency
    
  4. Batch related updates:

    // Updates are automatically batched
    firstName.value = 'John'
    lastName.value = 'Doe'
    // fullName updates only once
    
  5. 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.