Files
sigpro/docs/api/signal.md
2026-04-26 15:38:10 +02:00

10 KiB

The Signal Function: $( )

The $( ) function is the core constructor of SigPro. It defines how data is stored, computed, and persisted.

Function Signature

$(initialValue: any, key?: string): Signal
$(computation: Function): ComputedSignal
Parameter Type Required Description
initialValue any Yes* The starting value of your signal.
computation Function Yes* A function that returns a value based on other signals.
key string No A unique name to persist the signal in localStorage.

*Either an initial value or a computation function must be provided.


Usage Patterns

1. Simple State

$(value) Creates a writable signal. It returns a function that acts as both getter and setter.

{
  const count = $(0);
  const App = () => div({ class: "example" }, [
    p(() => `Count: ${count()}`),
    button({ onClick: () => count(count() + 1) }, "+1")
  ]);
  setTimeout(() => mount(App, '#demo-signal-simple'), 50);
}

2. Persistent State

$(value, key) Creates a writable signal that syncs with the browser's storage.

{
  const theme = $("light", "theme-persist-demo");
  const App = () => div([
    p(() => `Current theme: ${theme()}`),
    button({ onClick: () => theme(theme() === "light" ? "dark" : "light") }, "Toggle theme")
  ]);
  setTimeout(() => mount(App, '#demo-signal-persist'), 50);
}

Note: On page load, SigPro will prioritize the value found in localStorage over the initialValue.

3. Computed State (Derived)

$(function) Creates a read-only signal that updates automatically when any signal used inside it changes.

{
  const price = $(100);
  const tax = $(0.21);
  const total = $(() => price() * (1 + tax()));

  const App = () => div([
    p(() => `Price: €${price()}`),
    p(() => `Tax rate: ${tax() * 100}%`),
    p(() => `Total: €${total().toFixed(2)}`),
    button({ onClick: () => price(price() + 10) }, "+€10"),
    button({ onClick: () => price(price() - 10) }, "-€10")
  ]);
  setTimeout(() => mount(App, '#demo-signal-computed'), 50);
}

Updating with Logic

When calling the setter, you can pass an updater function to access the current value safely.

{
  const list = $(["A", "B"]);
  const App = () => div([
    ul(() => list().map(item => li(item))),
    button({ onClick: () => list(prev => [...prev, "C"]) }, "Add C")
  ]);
  setTimeout(() => mount(App, '#demo-signal-updater'), 50);
}

The Reactive Object: $$( )

The $$( ) function creates a reactive proxy for complex nested objects. Unlike $(), which tracks a single value, $$() tracks every property access automatically.

Function Signature

$$<T extends object>(obj: T): T
Parameter Type Required Description
obj object Yes The object to make reactive. Properties are tracked recursively.

Returns: A reactive proxy that behaves like the original object but triggers updates when any property changes.


Usage Patterns

1. Simple Object

{
  const state = $$({ count: 0, name: "Juan" });
  watch(() => console.log(`Count is now ${state.count}`));

  const App = () => div([
    p(() => `Count: ${state.count}, Name: ${state.name}`),
    button({ onClick: () => state.count++ }, "Increment count"),
    button({ onClick: () => state.name = state.name === "Juan" ? "Ana" : "Juan" }, "Toggle name")
  ]);
  setTimeout(() => mount(App, '#demo-dollar-simple'), 50);
}

2. Deep Reactivity

{
  const user = $$({
    profile: {
      name: "Juan",
      address: { city: "Madrid", zip: "28001" }
    }
  });

  watch(() => user.profile.address.city, () => console.log("City changed"));

  const App = () => div([
    p(() => `City: ${user.profile.address.city}`),
    button({ onClick: () => user.profile.address.city = "Barcelona" }, "Change to Barcelona")
  ]);
  setTimeout(() => mount(App, '#demo-dollar-deep'), 50);
}

3. Arrays

{
  const todos = $$([
    { id: 1, text: "Learn SigPro", done: false },
    { id: 2, text: "Build an app", done: false }
  ]);

  watch(() => todos.length, () => console.log(`You have ${todos.length} todos`));

  const App = () => div([
    ul(() => todos.map(todo => li(todo.text + (todo.done ? " ✓" : "")))),
    button({ onClick: () => todos.push({ id: Date.now(), text: "New todo", done: false }) }, "Add todo"),
    button({ onClick: () => todos[0].done = !todos[0].done }, "Toggle first todo")
  ]);
  setTimeout(() => mount(App, '#demo-dollar-array'), 50);
}

4. Mixed with Signals

{
  const form = $$({
    fields: { email: "", password: "" },
    isValid: $(false)
  });

  const canSubmit = $(() => 
    form.fields.email.includes("@") && 
    form.fields.password.length > 6
  );

  watch(canSubmit, valid => form.isValid(valid));

  const App = () => div([
    input({ type: "email", placeholder: "Email", value: () => form.fields.email, onInput: e => form.fields.email = e.target.value }),
    input({ type: "password", placeholder: "Password", value: () => form.fields.password, onInput: e => form.fields.password = e.target.value }),
    p(() => `Form valid: ${form.isValid() ? "Yes" : "No"}`)
  ]);
  setTimeout(() => mount(App, '#demo-dollar-mixed'), 50);
}

Key Differences: $() vs $$()

Feature $() Signal $$() Reactive
Primitives Works directly Needs wrapper object
Objects Manual tracking Automatic deep tracking
Nested properties Not reactive Fully reactive
Arrays Requires reassignment Methods (push, pop, etc.) work
Syntax count() / count(5) state.count = 5
LocalStorage Built-in (use $() for persistence)
Performance Lighter Slightly heavier (Proxy)
Destructuring Safe Breaks reactivity

When to Use Each

Use $() when:

{
  const count = $(0);
  const firstName = $("John");
  const lastName = $("Doe");
  const fullName = $(() => `${firstName()} ${lastName()}`);

  const App = () => div([
    p(() => `Count: ${count()}`),
    button({ onClick: () => count(count() + 1) }, "Count up"),
    p(() => `Full name: ${fullName()}`),
    input({ value: firstName, placeholder: "First name" }),
    input({ value: lastName, placeholder: "Last name" })
  ]);
  setTimeout(() => mount(App, '#demo-use-dollar'), 50);
}

Use $$() when:

{
  const form = $$({ email: "", password: "" });
  const settings = $$({ theme: "dark", notifications: true });

  const App = () => div([
    input({ placeholder: "Email", onInput: e => form.email = e.target.value }),
    input({ placeholder: "Password", type: "password", onInput: e => form.password = e.target.value }),
    p(() => `Email: ${form.email}, Password: ${form.password}`),
    button({ onClick: () => settings.theme = settings.theme === "dark" ? "light" : "dark" }, "Toggle theme"),
    p(() => `Current theme: ${settings.theme}`)
  ]);
  setTimeout(() => mount(App, '#demo-use-dollar-dollar'), 50);
}

Important Notes

DO:

// Access properties directly
state.count = 10;
state.user.name = "Ana";
todos.push(newItem);

// Track in effects
watch(() => state.count, () => {});
watch(() => state.user.name, () => {});

DON'T:

// Destructuring breaks reactivity
const { count, user } = state; // ❌ count and user are not reactive

// Reassigning the whole object
state = { count: 10 }; // ❌ Loses reactivity

// Using primitive directly
const count = $$(0); // ❌ Doesn't work (use $() instead)

Automatic Cleanup

Like all SigPro reactive primitives, $$() integrates with the cleanup system:

  • Effects tracking reactive properties are automatically disposed
  • No manual cleanup needed
  • Works with watch, when, and each

Technical Comparison

Aspect $() $$()
Implementation Closure with Set Proxy with WeakMap
Tracking Explicit (function call) Implicit (property access)
Memory Minimal Slightly more (WeakMap cache)
Use Case Simple state Complex state
Learning Curve Low Low (feels like plain JS)

Complete Example

{
  const app = {
    theme: $("dark", "theme_complete"),
    sidebarOpen: $(true),
    user: $$({ name: "", email: "", preferences: { notifications: true, language: "es" } }),
    isLoggedIn: $(() => !!app.user.name),
    login(name, email) {
      app.user.name = name;
      app.user.email = email;
    },
    logout() {
      app.user.name = "";
      app.user.email = "";
      app.user.preferences.notifications = true;
    }
  };

  const LoginForm = () => div([
    input({ placeholder: "Name", onInput: e => app.user.name = e.target.value }),
    input({ placeholder: "Email", onInput: e => app.user.email = e.target.value }),
    button({ onClick: () => app.login(app.user.name, app.user.email) }, "Login")
  ]);

  const UserProfile = () => div([
    h2(() => `Welcome ${app.user.name}`),
    p(() => `Email: ${app.user.email}`),
    p(() => `Notifications: ${app.user.preferences.notifications ? "ON" : "OFF"}`),
    button({ onClick: () => app.user.preferences.notifications = !app.user.preferences.notifications }, "Toggle Notifications"),
    button({ onClick: app.logout }, "Logout")
  ]);

  const App = () => div({ class: "complete-example" }, [
    when(() => app.isLoggedIn(), () => UserProfile(), () => LoginForm())
  ]);

  setTimeout(() => mount(App, '#demo-complete'), 50);
}

Migration from $() to $$()

If you have code using nested signals:

// Before - Manual nesting
const user = $({
  name: $(""),
  email: $("")
});
user().name("Juan"); // Need to call inner signal

// After - Automatic nesting
const user = $$({
  name: "",
  email: ""
});
user.name = "Juan"; // Direct assignment