900 lines
19 KiB
Markdown
900 lines
19 KiB
Markdown
# Signals API 📡
|
|
|
|
Signals are the heart of SigPro's reactivity system. They are reactive values that automatically track dependencies and notify subscribers when they change. This enables fine-grained updates without virtual DOM diffing.
|
|
|
|
## Core Concepts
|
|
|
|
### What is a Signal?
|
|
|
|
A signal is a function that holds a value and notifies dependents when that value changes. Signals can be:
|
|
|
|
- **Basic signals** - Hold simple values (numbers, strings, objects)
|
|
- **Computed signals** - Derive values from other signals
|
|
- **Persistent signals** - Automatically sync with localStorage/sessionStorage
|
|
|
|
### How Reactivity Works
|
|
|
|
SigPro uses automatic dependency tracking:
|
|
|
|
1. When you read a signal inside an effect, the effect becomes a subscriber
|
|
2. When the signal's value changes, all subscribers are notified
|
|
3. Updates are batched using microtasks for optimal performance
|
|
4. Only the exact nodes that depend on changed values are updated
|
|
|
|
## `$(initialValue)`
|
|
|
|
Creates a reactive signal. The behavior changes based on the type of `initialValue`:
|
|
|
|
- If `initialValue` is a **function**, creates a computed signal
|
|
- Otherwise, creates a basic signal
|
|
|
|
```javascript
|
|
import { $ } from 'sigpro';
|
|
|
|
// Basic signal
|
|
const count = $(0);
|
|
|
|
// Computed signal
|
|
const firstName = $('John');
|
|
const lastName = $('Doe');
|
|
const fullName = $(() => `${firstName()} ${lastName()}`);
|
|
```
|
|
|
|
## 📋 API Reference
|
|
|
|
### Basic Signals
|
|
|
|
| Pattern | Example | Description |
|
|
|---------|---------|-------------|
|
|
| Create | `const count = $(0)` | Create signal with initial value |
|
|
| Get | `count()` | Read current value |
|
|
| Set | `count(5)` | Set new value directly |
|
|
| Update | `count(prev => prev + 1)` | Update based on previous value |
|
|
|
|
### Computed Signals
|
|
|
|
| Pattern | Example | Description |
|
|
|---------|---------|-------------|
|
|
| Create | `const total = $(() => price() * quantity())` | Derive value from other signals |
|
|
| Get | `total()` | Read computed value (auto-updates) |
|
|
|
|
### Signal Methods
|
|
|
|
| Method | Description | Example |
|
|
|--------|-------------|---------|
|
|
| `signal()` | Gets current value | `count()` |
|
|
| `signal(newValue)` | Sets new value | `count(5)` |
|
|
| `signal(prev => new)` | Updates using previous value | `count(c => c + 1)` |
|
|
|
|
## 🎯 Basic Examples
|
|
|
|
### Counter Signal
|
|
|
|
```javascript
|
|
import { $ } from 'sigpro';
|
|
|
|
const count = $(0);
|
|
|
|
console.log(count()); // 0
|
|
|
|
count(5);
|
|
console.log(count()); // 5
|
|
|
|
count(prev => prev + 1);
|
|
console.log(count()); // 6
|
|
```
|
|
|
|
### Object Signal
|
|
|
|
```javascript
|
|
import { $ } from 'sigpro';
|
|
|
|
const user = $({
|
|
name: 'John',
|
|
age: 30,
|
|
email: 'john@example.com'
|
|
});
|
|
|
|
// Read
|
|
console.log(user().name); // 'John'
|
|
|
|
// Update (immutable pattern)
|
|
user({
|
|
...user(),
|
|
age: 31
|
|
});
|
|
|
|
// Partial update with function
|
|
user(prev => ({
|
|
...prev,
|
|
email: 'john.doe@example.com'
|
|
}));
|
|
```
|
|
|
|
### Array Signal
|
|
|
|
```javascript
|
|
import { $ } from 'sigpro';
|
|
|
|
const todos = $(['Learn SigPro', 'Build an app']);
|
|
|
|
// Add item
|
|
todos([...todos(), 'Deploy to production']);
|
|
|
|
// Remove item
|
|
todos(todos().filter((_, i) => i !== 1));
|
|
|
|
// Update item
|
|
todos(todos().map((todo, i) =>
|
|
i === 0 ? 'Master SigPro' : todo
|
|
));
|
|
```
|
|
|
|
## 🔄 Computed Signals
|
|
|
|
Computed signals automatically update when their dependencies change:
|
|
|
|
```javascript
|
|
import { $ } from 'sigpro';
|
|
|
|
const price = $(10);
|
|
const quantity = $(2);
|
|
const tax = $(0.21);
|
|
|
|
// Computed signals
|
|
const subtotal = $(() => price() * quantity());
|
|
const taxAmount = $(() => subtotal() * tax());
|
|
const total = $(() => subtotal() + taxAmount());
|
|
|
|
console.log(total()); // 24.2
|
|
|
|
price(15);
|
|
console.log(total()); // 36.3 (automatically updated)
|
|
|
|
quantity(3);
|
|
console.log(total()); // 54.45 (automatically updated)
|
|
```
|
|
|
|
### Computed with Multiple Dependencies
|
|
|
|
```javascript
|
|
import { $ } from 'sigpro';
|
|
|
|
const firstName = $('John');
|
|
const lastName = $('Doe');
|
|
const prefix = $('Mr.');
|
|
|
|
const fullName = $(() => {
|
|
// Computed signals can contain logic
|
|
const name = `${firstName()} ${lastName()}`;
|
|
return prefix() ? `${prefix()} ${name}` : name;
|
|
});
|
|
|
|
console.log(fullName()); // 'Mr. John Doe'
|
|
|
|
prefix('');
|
|
console.log(fullName()); // 'John Doe'
|
|
```
|
|
|
|
### Computed with Conditional Logic
|
|
|
|
```javascript
|
|
import { $ } from 'sigpro';
|
|
|
|
const user = $({ role: 'admin', permissions: [] });
|
|
const isAdmin = $(() => user().role === 'admin');
|
|
const hasPermission = $(() =>
|
|
isAdmin() || user().permissions.includes('edit')
|
|
);
|
|
|
|
console.log(hasPermission()); // true
|
|
|
|
user({ role: 'user', permissions: ['view'] });
|
|
console.log(hasPermission()); // false (can't edit)
|
|
|
|
user({ role: 'user', permissions: ['view', 'edit'] });
|
|
console.log(hasPermission()); // true (now has permission)
|
|
```
|
|
|
|
## 🧮 Advanced Signal Patterns
|
|
|
|
### Derived State Pattern
|
|
|
|
```javascript
|
|
import { $ } from 'sigpro';
|
|
|
|
// Shopping cart example
|
|
const cart = $([
|
|
{ id: 1, name: 'Product 1', price: 10, quantity: 2 },
|
|
{ id: 2, name: 'Product 2', price: 15, quantity: 1 },
|
|
]);
|
|
|
|
// Derived values
|
|
const itemCount = $(() =>
|
|
cart().reduce((sum, item) => sum + item.quantity, 0)
|
|
);
|
|
|
|
const subtotal = $(() =>
|
|
cart().reduce((sum, item) => sum + (item.price * item.quantity), 0)
|
|
);
|
|
|
|
const tax = $(() => subtotal() * 0.21);
|
|
const total = $(() => subtotal() + tax());
|
|
|
|
// Update cart
|
|
cart([
|
|
...cart(),
|
|
{ id: 3, name: 'Product 3', price: 20, quantity: 1 }
|
|
]);
|
|
|
|
// All derived values auto-update
|
|
console.log(itemCount()); // 4
|
|
console.log(total()); // (10*2 + 15*1 + 20*1) * 1.21 = 78.65
|
|
```
|
|
|
|
### Validation Pattern
|
|
|
|
```javascript
|
|
import { $ } from 'sigpro';
|
|
|
|
const email = $('');
|
|
const password = $('');
|
|
const confirmPassword = $('');
|
|
|
|
// Validation signals
|
|
const isEmailValid = $(() => {
|
|
const value = email();
|
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
|
});
|
|
|
|
const isPasswordValid = $(() => {
|
|
const value = password();
|
|
return value.length >= 8;
|
|
});
|
|
|
|
const doPasswordsMatch = $(() =>
|
|
password() === confirmPassword()
|
|
);
|
|
|
|
const isFormValid = $(() =>
|
|
isEmailValid() && isPasswordValid() && doPasswordsMatch()
|
|
);
|
|
|
|
// Update form
|
|
email('user@example.com');
|
|
password('secure123');
|
|
confirmPassword('secure123');
|
|
|
|
console.log(isFormValid()); // true
|
|
|
|
// Validation messages
|
|
const emailError = $(() =>
|
|
email() && !isEmailValid() ? 'Invalid email format' : ''
|
|
);
|
|
```
|
|
|
|
### Filtering and Search Pattern
|
|
|
|
```javascript
|
|
import { $ } from 'sigpro';
|
|
|
|
const items = $([
|
|
{ id: 1, name: 'Apple', category: 'fruit' },
|
|
{ id: 2, name: 'Banana', category: 'fruit' },
|
|
{ id: 3, name: 'Carrot', category: 'vegetable' },
|
|
{ id: 4, name: 'Date', category: 'fruit' },
|
|
]);
|
|
|
|
const searchTerm = $('');
|
|
const categoryFilter = $('all');
|
|
|
|
// Filtered items (computed)
|
|
const filteredItems = $(() => {
|
|
let result = items();
|
|
|
|
// Apply search filter
|
|
if (searchTerm()) {
|
|
const term = searchTerm().toLowerCase();
|
|
result = result.filter(item =>
|
|
item.name.toLowerCase().includes(term)
|
|
);
|
|
}
|
|
|
|
// Apply category filter
|
|
if (categoryFilter() !== 'all') {
|
|
result = result.filter(item =>
|
|
item.category === categoryFilter()
|
|
);
|
|
}
|
|
|
|
return result;
|
|
});
|
|
|
|
// Stats
|
|
const fruitCount = $(() =>
|
|
items().filter(item => item.category === 'fruit').length
|
|
);
|
|
|
|
const vegCount = $(() =>
|
|
items().filter(item => item.category === 'vegetable').length
|
|
);
|
|
|
|
// Update filters
|
|
searchTerm('a');
|
|
console.log(filteredItems().map(i => i.name)); // ['Apple', 'Banana', 'Carrot', 'Date']
|
|
|
|
categoryFilter('fruit');
|
|
console.log(filteredItems().map(i => i.name)); // ['Apple', 'Banana', 'Date']
|
|
```
|
|
|
|
### Pagination Pattern
|
|
|
|
```javascript
|
|
import { $ } from 'sigpro';
|
|
|
|
const allItems = $([...Array(100).keys()].map(i => `Item ${i + 1}`));
|
|
const currentPage = $(1);
|
|
const itemsPerPage = $(10);
|
|
|
|
// Paginated items (computed)
|
|
const paginatedItems = $(() => {
|
|
const start = (currentPage() - 1) * itemsPerPage();
|
|
const end = start + itemsPerPage();
|
|
return allItems().slice(start, end);
|
|
});
|
|
|
|
// Pagination metadata
|
|
const totalPages = $(() =>
|
|
Math.ceil(allItems().length / itemsPerPage())
|
|
);
|
|
|
|
const hasNextPage = $(() =>
|
|
currentPage() < totalPages()
|
|
);
|
|
|
|
const hasPrevPage = $(() =>
|
|
currentPage() > 1
|
|
);
|
|
|
|
const pageRange = $(() => {
|
|
const current = currentPage();
|
|
const total = totalPages();
|
|
const delta = 2;
|
|
|
|
let range = [];
|
|
for (let i = Math.max(2, current - delta);
|
|
i <= Math.min(total - 1, current + delta);
|
|
i++) {
|
|
range.push(i);
|
|
}
|
|
|
|
if (current - delta > 2) range = ['...', ...range];
|
|
if (current + delta < total - 1) range = [...range, '...'];
|
|
|
|
return [1, ...range, total];
|
|
});
|
|
|
|
// Navigation
|
|
const nextPage = () => {
|
|
if (hasNextPage()) currentPage(c => c + 1);
|
|
};
|
|
|
|
const prevPage = () => {
|
|
if (hasPrevPage()) currentPage(c => c - 1);
|
|
};
|
|
|
|
const goToPage = (page) => {
|
|
if (page >= 1 && page <= totalPages()) {
|
|
currentPage(page);
|
|
}
|
|
};
|
|
```
|
|
|
|
## 🔧 Advanced Signal Features
|
|
|
|
### Signal Equality Comparison
|
|
|
|
Signals use `Object.is` for change detection. Only notify subscribers when values are actually different:
|
|
|
|
```javascript
|
|
import { $ } from 'sigpro';
|
|
|
|
const count = $(0);
|
|
|
|
// These won't trigger updates:
|
|
count(0); // Same value
|
|
count(prev => prev); // Returns same value
|
|
|
|
// These will trigger updates:
|
|
count(1); // Different value
|
|
count(prev => prev + 0); // Still 0? Actually returns 0? Wait...
|
|
// Be careful with functional updates!
|
|
```
|
|
|
|
### Batch Updates
|
|
|
|
Multiple signal updates are batched into a single microtask:
|
|
|
|
```javascript
|
|
import { $ } from 'sigpro';
|
|
|
|
const firstName = $('John');
|
|
const lastName = $('Doe');
|
|
const fullName = $(() => `${firstName()} ${lastName()}`);
|
|
|
|
$.effect(() => {
|
|
console.log('Full name:', fullName());
|
|
});
|
|
// Logs: 'Full name: John Doe'
|
|
|
|
// Multiple updates in same tick - only one effect run!
|
|
firstName('Jane');
|
|
lastName('Smith');
|
|
// Only logs once: 'Full name: Jane Smith'
|
|
```
|
|
|
|
### Infinite Loop Protection
|
|
|
|
SigPro includes protection against infinite reactive loops:
|
|
|
|
```javascript
|
|
import { $ } from 'sigpro';
|
|
|
|
const a = $(1);
|
|
const b = $(2);
|
|
|
|
// This would create a loop, but SigPro prevents it
|
|
$.effect(() => {
|
|
a(b()); // Reading b
|
|
b(a()); // Reading a - loop detected!
|
|
});
|
|
// Throws: "SigPro: Infinite reactive loop detected."
|
|
```
|
|
|
|
## 📊 Performance Characteristics
|
|
|
|
| Operation | Complexity | Notes |
|
|
|-----------|------------|-------|
|
|
| Signal read | O(1) | Direct value access |
|
|
| Signal write | O(n) | n = number of subscribers |
|
|
| Computed read | O(1) or O(m) | m = computation complexity |
|
|
| Effect run | O(s) | s = number of signal reads |
|
|
|
|
## 🎯 Best Practices
|
|
|
|
### 1. Keep Signals Focused
|
|
|
|
```javascript
|
|
// ❌ Avoid large monolithic signals
|
|
const state = $({
|
|
user: null,
|
|
posts: [],
|
|
theme: 'light',
|
|
notifications: []
|
|
});
|
|
|
|
// ✅ Split into focused signals
|
|
const user = $(null);
|
|
const posts = $([]);
|
|
const theme = $('light');
|
|
const notifications = $([]);
|
|
```
|
|
|
|
### 2. Use Computed for Derived State
|
|
|
|
```javascript
|
|
// ❌ Don't compute in templates/effects
|
|
$.effect(() => {
|
|
const total = items().reduce((sum, i) => sum + i.price, 0);
|
|
updateUI(total);
|
|
});
|
|
|
|
// ✅ Compute with signals
|
|
const total = $(() => items().reduce((sum, i) => sum + i.price, 0));
|
|
$.effect(() => updateUI(total()));
|
|
```
|
|
|
|
### 3. Immutable Updates
|
|
|
|
```javascript
|
|
// ❌ Don't mutate objects/arrays
|
|
const user = $({ name: 'John' });
|
|
user().name = 'Jane'; // Won't trigger updates!
|
|
|
|
// ✅ Create new objects/arrays
|
|
user({ ...user(), name: 'Jane' });
|
|
|
|
// ❌ Don't mutate arrays
|
|
const todos = $(['a', 'b']);
|
|
todos().push('c'); // Won't trigger updates!
|
|
|
|
// ✅ Create new arrays
|
|
todos([...todos(), 'c']);
|
|
```
|
|
|
|
### 4. Functional Updates for Dependencies
|
|
|
|
```javascript
|
|
// ❌ Avoid if new value depends on current
|
|
count(count() + 1);
|
|
|
|
// ✅ Use functional update
|
|
count(prev => prev + 1);
|
|
```
|
|
|
|
### 5. Clean Up Effects
|
|
|
|
```javascript
|
|
import { $ } from 'sigpro';
|
|
|
|
const userId = $(1);
|
|
|
|
// Effects auto-clean in pages, but you can stop manually
|
|
const stop = $.effect(() => {
|
|
fetchUser(userId());
|
|
});
|
|
|
|
// Later, if needed
|
|
stop();
|
|
```
|
|
|
|
## 🚀 Real-World Examples
|
|
|
|
### Form State Management
|
|
|
|
```javascript
|
|
import { $ } from 'sigpro';
|
|
|
|
// Form state
|
|
const formData = $({
|
|
username: '',
|
|
email: '',
|
|
age: '',
|
|
newsletter: false
|
|
});
|
|
|
|
// Touched fields (for validation UI)
|
|
const touched = $({
|
|
username: false,
|
|
email: false,
|
|
age: false
|
|
});
|
|
|
|
// Validation rules
|
|
const validations = {
|
|
username: (value) =>
|
|
value.length >= 3 ? null : 'Username must be at least 3 characters',
|
|
email: (value) =>
|
|
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? null : 'Invalid email',
|
|
age: (value) =>
|
|
!value || (value >= 18 && value <= 120) ? null : 'Age must be 18-120'
|
|
};
|
|
|
|
// Validation signals
|
|
const errors = $(() => {
|
|
const data = formData();
|
|
const result = {};
|
|
|
|
Object.keys(validations).forEach(field => {
|
|
const error = validations[field](data[field]);
|
|
if (error) result[field] = error;
|
|
});
|
|
|
|
return result;
|
|
});
|
|
|
|
const isValid = $(() => Object.keys(errors()).length === 0);
|
|
|
|
// Field helpers
|
|
const fieldProps = (field) => ({
|
|
value: formData()[field],
|
|
error: touched()[field] ? errors()[field] : null,
|
|
onChange: (e) => {
|
|
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
|
|
formData({
|
|
...formData(),
|
|
[field]: value
|
|
});
|
|
},
|
|
onBlur: () => {
|
|
touched({
|
|
...touched(),
|
|
[field]: true
|
|
});
|
|
}
|
|
});
|
|
|
|
// Form submission
|
|
const submitAttempts = $(0);
|
|
const isSubmitting = $(false);
|
|
|
|
const handleSubmit = async () => {
|
|
submitAttempts(s => s + 1);
|
|
|
|
if (!isValid()) {
|
|
// Mark all fields as touched to show errors
|
|
touched(Object.keys(formData()).reduce((acc, field) => ({
|
|
...acc,
|
|
[field]: true
|
|
}), {}));
|
|
return;
|
|
}
|
|
|
|
isSubmitting(true);
|
|
try {
|
|
await saveForm(formData());
|
|
// Reset form on success
|
|
formData({ username: '', email: '', age: '', newsletter: false });
|
|
touched({ username: false, email: false, age: false });
|
|
} finally {
|
|
isSubmitting(false);
|
|
}
|
|
};
|
|
```
|
|
|
|
### Todo App with Filters
|
|
|
|
```javascript
|
|
import { $ } from 'sigpro';
|
|
|
|
// State
|
|
const todos = $([
|
|
{ id: 1, text: 'Learn SigPro', completed: true },
|
|
{ id: 2, text: 'Build an app', completed: false },
|
|
{ id: 3, text: 'Write docs', completed: false }
|
|
]);
|
|
|
|
const filter = $('all'); // 'all', 'active', 'completed'
|
|
const newTodoText = $('');
|
|
|
|
// Computed values
|
|
const filteredTodos = $(() => {
|
|
const all = todos();
|
|
|
|
switch(filter()) {
|
|
case 'active':
|
|
return all.filter(t => !t.completed);
|
|
case 'completed':
|
|
return all.filter(t => t.completed);
|
|
default:
|
|
return all;
|
|
}
|
|
});
|
|
|
|
const activeCount = $(() =>
|
|
todos().filter(t => !t.completed).length
|
|
);
|
|
|
|
const completedCount = $(() =>
|
|
todos().filter(t => t.completed).length
|
|
);
|
|
|
|
const hasCompleted = $(() => completedCount() > 0);
|
|
|
|
// Actions
|
|
const addTodo = () => {
|
|
const text = newTodoText().trim();
|
|
if (text) {
|
|
todos([
|
|
...todos(),
|
|
{
|
|
id: Date.now(),
|
|
text,
|
|
completed: false
|
|
}
|
|
]);
|
|
newTodoText('');
|
|
}
|
|
};
|
|
|
|
const toggleTodo = (id) => {
|
|
todos(todos().map(todo =>
|
|
todo.id === id
|
|
? { ...todo, completed: !todo.completed }
|
|
: todo
|
|
));
|
|
};
|
|
|
|
const deleteTodo = (id) => {
|
|
todos(todos().filter(todo => todo.id !== id));
|
|
};
|
|
|
|
const clearCompleted = () => {
|
|
todos(todos().filter(todo => !todo.completed));
|
|
};
|
|
|
|
const toggleAll = () => {
|
|
const allCompleted = activeCount() === 0;
|
|
todos(todos().map(todo => ({
|
|
...todo,
|
|
completed: !allCompleted
|
|
})));
|
|
};
|
|
```
|
|
|
|
### Shopping Cart
|
|
|
|
```javascript
|
|
import { $ } from 'sigpro';
|
|
|
|
// Products catalog
|
|
const products = $([
|
|
{ id: 1, name: 'Laptop', price: 999, stock: 5 },
|
|
{ id: 2, name: 'Mouse', price: 29, stock: 20 },
|
|
{ id: 3, name: 'Keyboard', price: 79, stock: 10 },
|
|
{ id: 4, name: 'Monitor', price: 299, stock: 3 }
|
|
]);
|
|
|
|
// Cart state
|
|
const cart = $({});
|
|
const selectedProduct = $(null);
|
|
const quantity = $(1);
|
|
|
|
// Computed cart values
|
|
const cartItems = $(() => {
|
|
const items = [];
|
|
Object.entries(cart()).forEach(([productId, qty]) => {
|
|
const product = products().find(p => p.id === parseInt(productId));
|
|
if (product) {
|
|
items.push({
|
|
...product,
|
|
quantity: qty,
|
|
subtotal: product.price * qty
|
|
});
|
|
}
|
|
});
|
|
return items;
|
|
});
|
|
|
|
const itemCount = $(() =>
|
|
cartItems().reduce((sum, item) => sum + item.quantity, 0)
|
|
);
|
|
|
|
const subtotal = $(() =>
|
|
cartItems().reduce((sum, item) => sum + item.subtotal, 0)
|
|
);
|
|
|
|
const tax = $(() => subtotal() * 0.10);
|
|
const shipping = $(() => subtotal() > 100 ? 0 : 10);
|
|
const total = $(() => subtotal() + tax() + shipping());
|
|
|
|
const isCartEmpty = $(() => itemCount() === 0);
|
|
|
|
// Cart actions
|
|
const addToCart = (product, qty = 1) => {
|
|
const currentQty = cart()[product.id] || 0;
|
|
const newQty = currentQty + qty;
|
|
|
|
if (newQty <= product.stock) {
|
|
cart({
|
|
...cart(),
|
|
[product.id]: newQty
|
|
});
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
const updateQuantity = (productId, newQty) => {
|
|
const product = products().find(p => p.id === productId);
|
|
if (newQty <= product.stock) {
|
|
if (newQty <= 0) {
|
|
removeFromCart(productId);
|
|
} else {
|
|
cart({
|
|
...cart(),
|
|
[productId]: newQty
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
const removeFromCart = (productId) => {
|
|
const newCart = { ...cart() };
|
|
delete newCart[productId];
|
|
cart(newCart);
|
|
};
|
|
|
|
const clearCart = () => cart({});
|
|
|
|
// Stock management
|
|
const productStock = (productId) => {
|
|
const product = products().find(p => p.id === productId);
|
|
if (!product) return 0;
|
|
const inCart = cart()[productId] || 0;
|
|
return product.stock - inCart;
|
|
};
|
|
|
|
const isInStock = (productId, qty = 1) => {
|
|
return productStock(productId) >= qty;
|
|
};
|
|
```
|
|
|
|
## 📈 Debugging Signals
|
|
|
|
### Logging Signal Changes
|
|
|
|
```javascript
|
|
import { $ } from 'sigpro';
|
|
|
|
// Wrap a signal to log changes
|
|
const withLogging = (signal, name) => {
|
|
return (...args) => {
|
|
if (args.length) {
|
|
const oldValue = signal();
|
|
const result = signal(...args);
|
|
console.log(`${name}:`, oldValue, '->', signal());
|
|
return result;
|
|
}
|
|
return signal();
|
|
};
|
|
};
|
|
|
|
// Usage
|
|
const count = withLogging($(0), 'count');
|
|
count(5); // Logs: "count: 0 -> 5"
|
|
```
|
|
|
|
### Signal Inspector
|
|
|
|
```javascript
|
|
import { $ } from 'sigpro';
|
|
|
|
// Create an inspectable signal
|
|
const createInspector = () => {
|
|
const signals = new Map();
|
|
|
|
const createSignal = (initialValue, name) => {
|
|
const signal = $(initialValue);
|
|
signals.set(signal, { name, subscribers: new Set() });
|
|
|
|
// Wrap to track subscribers
|
|
const wrapped = (...args) => {
|
|
if (!args.length && activeEffect) {
|
|
const info = signals.get(wrapped);
|
|
info.subscribers.add(activeEffect);
|
|
}
|
|
return signal(...args);
|
|
};
|
|
|
|
return wrapped;
|
|
};
|
|
|
|
const getInfo = () => {
|
|
const info = {};
|
|
signals.forEach((data, signal) => {
|
|
info[data.name] = {
|
|
subscribers: data.subscribers.size,
|
|
value: signal()
|
|
};
|
|
});
|
|
return info;
|
|
};
|
|
|
|
return { createSignal, getInfo };
|
|
};
|
|
|
|
// Usage
|
|
const inspector = createInspector();
|
|
const count = inspector.createSignal(0, 'count');
|
|
const doubled = inspector.createSignal(() => count() * 2, 'doubled');
|
|
|
|
console.log(inspector.getInfo());
|
|
// { count: { subscribers: 0, value: 0 }, doubled: { subscribers: 0, value: 0 } }
|
|
```
|
|
|
|
## 📊 Summary
|
|
|
|
| Feature | Description |
|
|
|---------|-------------|
|
|
| **Basic Signals** | Hold values and notify on change |
|
|
| **Computed Signals** | Auto-updating derived values |
|
|
| **Automatic Tracking** | Dependencies tracked automatically |
|
|
| **Batch Updates** | Multiple updates batched in microtask |
|
|
| **Infinite Loop Protection** | Prevents reactive cycles |
|
|
| **Zero Dependencies** | Pure vanilla JavaScript |
|
|
|
|
---
|
|
|
|
> **Pro Tip:** Signals are the foundation of reactivity in SigPro. Master them, and you've mastered 80% of the library!
|