Skip to content

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
typescript
import { createSignal, reactive } from 'bindra';

Signal

createSignal()

Creates a reactive signal that holds a single value.

typescript
function createSignal<T>(initialValue: T): Signal<T>

Parameters

  • initialValue (T) - The initial value of the signal

Returns

A Signal<T> object with three methods:

typescript
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

typescript
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.

typescript
get(): T

Returns: Current value

Example:

typescript
const name = createSignal('Alice');
console.log(name.get()); // "Alice"

set()

Sets a new value for the signal and notifies all subscribers.

typescript
set(newValue: T): void

Parameters:

  • newValue - The new value to set

Example:

typescript
const count = createSignal(0);
count.set(1); // All subscribers notified
count.set(2); // All subscribers notified

Note: Subscribers are only notified if the value actually changes.

typescript
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.

typescript
subscribe(callback: (value: T) => void): () => void

Parameters:

  • callback - Function called when value changes

Returns: Unsubscribe function

Example:

typescript
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.

typescript
function reactive<T extends object>(
  obj: T,
  onChange?: (prop: string | symbol, value: any, old: any) => void
): T

Parameters

  • obj (T) - Object to make reactive
  • onChange (optional) - Callback for property changes

Returns

Reactive proxy of the object

Example

typescript
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:

typescript
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'; // Reactive

Array Support

Arrays are also reactive:

typescript
const state = reactive({
  items: [1, 2, 3]
});

state.items.push(4); // Reactive
state.items[0] = 10; // Reactive

Property Deletion

Property deletions are tracked:

typescript
const state = reactive({ name: 'Alice', age: 25 });

delete state.age; // Triggers onChange

Usage Examples

Counter Example

typescript
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

typescript
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

typescript
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

vue
<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

typescript
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 notified

Computed Values

While Bindra doesn't have built-in computed values, you can create them:

typescript
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

typescript
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:

typescript
// ✅ Good
const unsubscribe = signal.subscribe(callback);

// Later...
unsubscribe();

// ❌ Bad - Memory leak
signal.subscribe(callback);
// Never unsubscribed

2. Use Signals for Simple Values

typescript
// ✅ 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

typescript
// ✅ 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

typescript
// ✅ Good - Only update when needed
if (newValue !== signal.get()) {
  signal.set(newValue);
}

// ❌ Bad - Always updates
signal.set(signal.get()); // Unnecessary

5. Use onChange for Side Effects

typescript
// ✅ 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 tracked


← Back to API Reference

Released under the MIT License.