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, andFor
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