Files
sigpro/docs/api/signal.md
2026-04-06 18:28:07 +02:00

7.8 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); 

count();    // Read (0)
count(10);  // Write (10)

2. Persistent State

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

const theme = $("light", "app-theme"); 

theme("dark"); // Automatically calls localStorage.setItem("app-theme", '"dark"')

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);

// This tracks both 'price' and 'tax' automatically
const total = $(() => price() * (1 + tax())); 

Updating with Logic

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

const list = $(["A", "B"]);

// Adds "C" using the previous state
list(prev => [...prev, "C"]); 

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(() => state.count, () => {
  console.log(`Count is now ${state.count}`);
});

state.count++; // ✅ Triggers update
state.name = "Ana"; // ✅ Also reactive

2. Deep Reactivity

Unlike $(), $$() tracks nested properties automatically.

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

// This works! Tracks deep property access
Watch(() => user.profile.address.city, () => {
  console.log("City changed");
});

user.profile.address.city = "Barcelona"; // ✅ Triggers update

3. Arrays

$$() works with arrays and array methods.

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`);
});

// Array methods are reactive
todos.push({ id: 3, text: "Deploy", done: false }); // ✅ Triggers
todos[0].done = true; // ✅ Deep reactivity works
todos.splice(1, 1); // ✅ Triggers

4. Mixed with Signals

$$() works seamlessly with $() signals.

const form = $$({
  fields: {
    email: "",
    password: ""
  },
  isValid: $(false) // Signal inside reactive object
});

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

Watch(canSubmit, (valid) => {
  form.isValid(valid); // Update signal inside reactive object
});

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:

  • Working with primitives (numbers, strings, booleans)
  • Need localStorage persistence
  • Creating computed values
  • Want explicit control over updates
const count = $(0);
const user = $(null);
const fullName = $(() => `${firstName()} ${lastName()}`);

Use $$() when:

  • Working with complex nested objects
  • Managing forms with multiple fields
  • Using arrays with mutations (push, pop, splice)
  • Want natural object syntax (no function calls)
const form = $$({ email: "", password: "" });
const settings = $$({ theme: "dark", notifications: true });
const store = $$({ users: [], filters: {}, pagination: { page: 1 } });

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, If, and For

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

// Combining both approaches
const app = {
  // Simple primitives with persistence
  theme: $("dark", "theme"),
  sidebarOpen: $(true),
  
  // Complex state with $$()
  user: $$({
    name: "",
    email: "",
    preferences: {
      notifications: true,
      language: "es"
    }
  }),
  
  // Computed values
  isLoggedIn: $(() => !!app.user.name),
  
  // Actions
  login(name, email) {
    app.user.name = name;
    app.user.email = email;
  },
  
  logout() {
    app.user.name = "";
    app.user.email = "";
    app.user.preferences.notifications = true;
  }
};

// UI component
const UserProfile = () => {
  return Div({}, [
    If(() => app.isLoggedIn(),
      () => 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")
      ]),
      () => LoginForm()
    )
  ]);
};

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