Files
sigpro/docs/api/signal.md
natxocc 439809b1e7
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 9s
Modular router && remove $$
2026-05-05 16:27:53 +02:00

9.8 KiB

The Signal Function: $()

The $() function is the only reactive primitive in SigPro. It defines how data is stored, computed, and persisted. For complex nested objects, you compose signals naturally.

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

Composing Signals for Complex State

For nested objects, compose signals instead of using magic proxies. This gives you explicit control over reactivity and memory.

1. Simple Object

{
  const count = $(0);
  const name = $("Juan");
  
  // Optionally create a derived combined state
  const state = $(() => ({ count: count(), name: name() }));

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

2. Deeply Nested State

{
  const profileName = $("Juan");
  const profileCity = $("Madrid");
  const profileZip = $("28001");

  // Computed derived values
  const fullAddress = $(() => `${profileCity()}, ${profileZip()}`);
  
  watch(profileCity, () => console.log("City changed to:", profileCity()));

  const App = () => div([
    p(() => `Name: ${profileName()}`),
    p(() => `City: ${profileCity()}`),
    p(() => `Full address: ${fullAddress()}`),
    button({ onClick: () => profileCity("Barcelona") }, "Change to Barcelona")
  ]);
  setTimeout(() => mount(App, '#demo-compose-deep'), 50);
}

3. Arrays

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

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

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

4. Complete Form Example

{
  const email = $("");
  const password = $("");
  const isValid = $(() => email().includes("@") && password().length > 6);

  watch(isValid, valid => console.log("Form valid:", valid));

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

Best Practices for Complex State

DO: Compose signals explicitly

// Clear, predictable, and memory-safe
const user = {
  name: $("Juan"),
  email: $("juan@example.com"),
  preferences: {
    theme: $("dark"),
    notifications: $(true)
  }
}

// Computed values derived from composition
const userDisplay = $(() => `${user.name()} <${user.email()}>`)

DO: Create store patterns

const createUserStore = () => {
  const name = $("")
  const email = $("")
  
  const isValid = $(() => name().length > 0 && email().includes("@"))
  
  const actions = {
    setName: (value) => name(value),
    setEmail: (value) => email(value),
    reset: () => {
      name("")
      email("")
    }
  }
  
  return { name, email, isValid, ...actions }
}

const userStore = createUserStore()

DON'T: Try to wrap objects with signals

// Wrong - loses reactivity on nested properties
const user = $({ name: "Juan", email: "..." })
user().name = "Ana" // ❌ Not reactive!

// Correct - each property its own signal
const userName = $("Juan")
const userEmail = $("...")

DON'T: Destructure signals in reactive contexts

// Wrong - breaks tracking
const { name, email } = user
watch(() => name(), ...) // ❌ 'name' is not tracked properly

// Correct - use the original signal
watch(() => user.name(), ...) // ✅

Important Notes

DO:

// Update by recreating objects for arrays
todos(prev => [...prev, newTodo])

// Update objects immutably
const current = user()
user({ ...current, name: "Ana" })

// Track individual signals
watch(() => user.name(), () => {})
watch(() => user.email(), () => {})

DON'T:

// Mutate objects directly
user().name = "Ana" // ❌ Not reactive

// Mutate arrays in place
todos().push(newTodo) // ❌ Not reactive

// Destructure in component bodies
const { name, email } = user // ❌ Breaks reactivity

Automatic Cleanup

All signals integrate with the cleanup system:

// Effects are automatically disposed when components unmount
const name = $("Juan")
watch(name, () => console.log("Name changed"))

// Manual cleanup if needed
const stop = watch(name, callback)
stop() // Clean up manually

Complete Example

{
  // All state as explicit signals
  const theme = $("dark", "theme_complete")
  const sidebarOpen = $(true)
  const userName = $("")
  const userEmail = $("")
  const notifications = $(true)
  const language = $("es")
  
  // Computed signals
  const isLoggedIn = $(() => !!userName() && !!userEmail())
  
  // Actions as plain functions
  const login = (name, email) => {
    userName(name)
    userEmail(email)
  }
  
  const logout = () => {
    userName("")
    userEmail("")
    notifications(true) // Reset on logout
  }
  
  // Components using signals directly
  const LoginForm = () => div([
    input({ 
      placeholder: "Name", 
      onInput: e => userName(e.target.value) 
    }),
    input({ 
      placeholder: "Email", 
      onInput: e => userEmail(e.target.value) 
    }),
    button({ 
      onClick: () => login(userName(), userEmail()) 
    }, "Login")
  ])

  const UserProfile = () => div([
    h2(() => `Welcome ${userName()}`),
    p(() => `Email: ${userEmail()}`),
    p(() => `Notifications: ${notifications() ? "ON" : "OFF"}`),
    p(() => `Language: ${language()}`),
    button({ 
      onClick: () => notifications(!notifications()) 
    }, "Toggle Notifications"),
    button({ onClick: logout }, "Logout")
  ])

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

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

Summary

With only $() as your reactive primitive:

  • Explicit - You know exactly what's reactive
  • Memory safe - No hidden proxies or WeakMap caches
  • Predictable - No magic, just signals
  • Performant - Minimal overhead
  • Debuggable - Clear data flow

Complex state is built by composing signals, not by wrapping objects. This gives you the same power as reactive proxies but with better control and fewer surprises.