Observable API Reference
The Observable module provides reactive primitives including signals and reactive objects for building reactive applications.
Table of Contents
Overview
Bindra's reactivity system is inspired by Vue 3 reactivity and Solid.js signals. It provides:
- Signals - Simple reactive values
- Reactive Objects - Deep reactive objects with nested reactivity
- Subscriptions - Listen to changes
- Automatic Updates - UI updates automatically when data changes
import { createSignal, reactive } from 'bindra';Signal
createSignal()
Creates a reactive signal that holds a single value.
function createSignal<T>(initialValue: T): Signal<T>Parameters
- initialValue (
T) - The initial value of the signal
Returns
A Signal<T> object with three methods:
interface Signal<T> {
get(): T; // Get current value
set(newValue: T): void; // Set new value
subscribe(fn: (value: T) => void): () => void; // Subscribe to changes
}Example
import { createSignal } from 'bindra';
// Create a signal
const count = createSignal(0);
// Subscribe to changes
const unsubscribe = count.subscribe((value) => {
console.log('Count changed:', value);
});
// Immediately logs: "Count changed: 0"
// Get current value
console.log(count.get()); // 0
// Set new value
count.set(1);
// Logs: "Count changed: 1"
count.set(2);
// Logs: "Count changed: 2"
// Unsubscribe
unsubscribe();
count.set(3);
// Nothing logged (unsubscribed)Signal Methods
get()
Gets the current value of the signal.
get(): TReturns: Current value
Example:
const name = createSignal('Alice');
console.log(name.get()); // "Alice"set()
Sets a new value for the signal and notifies all subscribers.
set(newValue: T): voidParameters:
- newValue - The new value to set
Example:
const count = createSignal(0);
count.set(1); // All subscribers notified
count.set(2); // All subscribers notifiedNote: Subscribers are only notified if the value actually changes.
const count = createSignal(5);
count.subscribe((val) => console.log('Changed:', val));
// Logs: "Changed: 5"
count.set(5); // No log - value didn't change
count.set(10); // Logs: "Changed: 10"subscribe()
Subscribes to signal changes.
subscribe(callback: (value: T) => void): () => voidParameters:
- callback - Function called when value changes
Returns: Unsubscribe function
Example:
const count = createSignal(0);
const unsubscribe = count.subscribe((value) => {
console.log('New value:', value);
});
// Immediately logs: "New value: 0"
count.set(1); // Logs: "New value: 1"
count.set(2); // Logs: "New value: 2"
// Cleanup
unsubscribe();Important: The callback is called immediately with the current value when you subscribe.
Reactive Objects
reactive()
Creates a deeply reactive proxy object that tracks all property changes.
function reactive<T extends object>(
obj: T,
onChange?: (prop: string | symbol, value: any, old: any) => void
): TParameters
- obj (
T) - Object to make reactive - onChange (optional) - Callback for property changes
Returns
Reactive proxy of the object
Example
import { reactive } from 'bindra';
interface User {
name: string;
age: number;
address: {
city: string;
country: string;
};
}
// Create reactive object
const user = reactive<User>({
name: 'Alice',
age: 25,
address: {
city: 'New York',
country: 'USA'
}
});
// Listen to all changes
const reactiveUser = reactive(user, (prop, value, old) => {
console.log(`${String(prop)} changed from ${old} to ${value}`);
});
// Modify properties
user.name = 'Bob';
// Logs: "name changed from Alice to Bob"
user.age = 26;
// Logs: "age changed from 25 to 26"
// Nested reactivity
user.address.city = 'Boston';
// Logs: "city changed from New York to Boston"Reactive Features
Deep Reactivity
Nested objects are automatically made reactive:
const state = reactive({
user: {
profile: {
name: 'Alice',
settings: {
theme: 'dark'
}
}
}
});
// All levels are reactive
state.user.profile.name = 'Bob'; // Reactive
state.user.profile.settings.theme = 'light'; // ReactiveArray Support
Arrays are also reactive:
const state = reactive({
items: [1, 2, 3]
});
state.items.push(4); // Reactive
state.items[0] = 10; // ReactiveProperty Deletion
Property deletions are tracked:
const state = reactive({ name: 'Alice', age: 25 });
delete state.age; // Triggers onChangeUsage Examples
Counter Example
import { createSignal } from 'bindra';
const count = createSignal(0);
// Update UI
count.subscribe((value) => {
document.getElementById('counter')!.textContent = String(value);
});
// Increment button
document.getElementById('increment')!.onclick = () => {
count.set(count.get() + 1);
};
// Decrement button
document.getElementById('decrement')!.onclick = () => {
count.set(count.get() - 1);
};Form State
import { reactive } from 'bindra';
interface FormData {
username: string;
email: string;
password: string;
}
const form = reactive<FormData>({
username: '',
email: '',
password: ''
}, (prop, value) => {
console.log(`${String(prop)} updated to:`, value);
validateField(String(prop), value);
});
// Bind to inputs
document.getElementById('username')!.addEventListener('input', (e) => {
form.username = (e.target as HTMLInputElement).value;
});
document.getElementById('email')!.addEventListener('input', (e) => {
form.email = (e.target as HTMLInputElement).value;
});React Integration
import { createSignal } from 'bindra';
import { useEffect, useState } from 'react';
function useSignal<T>(signal: Signal<T>) {
const [value, setValue] = useState(signal.get());
useEffect(() => {
const unsubscribe = signal.subscribe(setValue);
return unsubscribe;
}, [signal]);
return value;
}
// Usage in component
function Counter() {
const count = createSignal(0);
const value = useSignal(count);
return (
<div>
<p>Count: {value}</p>
<button onClick={() => count.set(value + 1)}>
Increment
</button>
</div>
);
}Vue Integration
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { createSignal } from 'bindra';
const count = createSignal(0);
const value = ref(count.get());
let unsubscribe: (() => void) | null = null;
onMounted(() => {
unsubscribe = count.subscribe((val) => {
value.value = val;
});
});
onUnmounted(() => {
unsubscribe?.();
});
const increment = () => {
count.set(count.get() + 1);
};
</script>
<template>
<div>
<p>Count: {{ value }}</p>
<button @click="increment">Increment</button>
</div>
</template>Multiple Subscribers
import { createSignal } from 'bindra';
const theme = createSignal<'light' | 'dark'>('light');
// Multiple subscribers
theme.subscribe((value) => {
document.body.className = value;
});
theme.subscribe((value) => {
localStorage.setItem('theme', value);
});
theme.subscribe((value) => {
console.log('Theme changed to:', value);
});
// Update theme
theme.set('dark');
// All three subscribers are notifiedComputed Values
While Bindra doesn't have built-in computed values, you can create them:
import { createSignal } from 'bindra';
const firstName = createSignal('Alice');
const lastName = createSignal('Smith');
const fullName = createSignal('');
// Update fullName when either name changes
firstName.subscribe(() => {
fullName.set(`${firstName.get()} ${lastName.get()}`);
});
lastName.subscribe(() => {
fullName.set(`${firstName.get()} ${lastName.get()}`);
});
// Use fullName
fullName.subscribe((name) => {
console.log('Full name:', name);
});
firstName.set('Bob'); // Logs: "Full name: Bob Smith"
lastName.set('Jones'); // Logs: "Full name: Bob Jones"State Management
import { reactive } from 'bindra';
interface AppState {
user: {
name: string;
isLoggedIn: boolean;
};
settings: {
theme: 'light' | 'dark';
language: string;
};
notifications: string[];
}
const state = reactive<AppState>(
{
user: {
name: '',
isLoggedIn: false
},
settings: {
theme: 'light',
language: 'en'
},
notifications: []
},
(prop, value, old) => {
console.log('State changed:', { prop, value, old });
// Persist to localStorage
localStorage.setItem('appState', JSON.stringify(state));
}
);
// Login
function login(name: string) {
state.user.name = name;
state.user.isLoggedIn = true;
}
// Change theme
function setTheme(theme: 'light' | 'dark') {
state.settings.theme = theme;
}
// Add notification
function notify(message: string) {
state.notifications.push(message);
}Best Practices
1. Clean Up Subscriptions
Always unsubscribe when done:
// ✅ Good
const unsubscribe = signal.subscribe(callback);
// Later...
unsubscribe();
// ❌ Bad - Memory leak
signal.subscribe(callback);
// Never unsubscribed2. Use Signals for Simple Values
// ✅ Good - Signal for simple value
const count = createSignal(0);
const name = createSignal('Alice');
// ❌ Bad - Don't use reactive for primitives
const count = reactive({ value: 0 });3. Use Reactive for Complex Objects
// ✅ Good - Reactive for objects
const user = reactive({
name: 'Alice',
age: 25,
address: { city: 'NYC' }
});
// ❌ Bad - Too many signals
const userName = createSignal('Alice');
const userAge = createSignal(25);
const userCity = createSignal('NYC');4. Avoid Unnecessary Updates
// ✅ Good - Only update when needed
if (newValue !== signal.get()) {
signal.set(newValue);
}
// ❌ Bad - Always updates
signal.set(signal.get()); // Unnecessary5. Use onChange for Side Effects
// ✅ Good - Centralized change handler
const state = reactive(data, (prop, value) => {
// All changes go through here
logChange(prop, value);
validateField(prop, value);
updateUI(prop, value);
});
// ❌ Bad - Scattered logic
const state = reactive(data);
// Changes not trackedRelated Documentation
- DataSource - Uses signals internally
- EventEmitter - Event-based updates
- Getting Started - Reactivity basics