Add $$ proxies + Svg compatible + $if transaction
This commit is contained in:
@@ -66,3 +66,276 @@ 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
|
||||
|
||||
```typescript
|
||||
$$<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
|
||||
|
||||
```javascript
|
||||
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.
|
||||
|
||||
```javascript
|
||||
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.
|
||||
|
||||
```javascript
|
||||
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.
|
||||
|
||||
```javascript
|
||||
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
|
||||
|
||||
```javascript
|
||||
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)
|
||||
|
||||
```javascript
|
||||
const form = $$({ email: "", password: "" });
|
||||
const settings = $$({ theme: "dark", notifications: true });
|
||||
const store = $$({ users: [], filters: {}, pagination: { page: 1 } });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Important Notes
|
||||
|
||||
### ✅ DO:
|
||||
```javascript
|
||||
// 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:
|
||||
```javascript
|
||||
// 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
|
||||
|
||||
```javascript
|
||||
// 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:
|
||||
|
||||
```javascript
|
||||
// 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
|
||||
```
|
||||
Reference in New Issue
Block a user