Storage API 💾
SigPro provides persistent signals that automatically synchronize with browser storage APIs. This allows you to create reactive state that survives page reloads and browser sessions with zero additional code.
Core Concepts
What is Persistent Storage?
Persistent signals are special signals that:
- Initialize from storage (localStorage/sessionStorage) if a saved value exists
- Auto-save whenever the signal value changes
- Handle JSON serialization automatically
- Clean up when set to
nullorundefined
Storage Types
| Storage | Persistence | Use Case |
|---|---|---|
localStorage | Forever (until cleared) | User preferences, themes, saved data |
sessionStorage | Until tab/window closes | Form drafts, temporary state |
$.storage(key, initialValue, [storage])
Creates a persistent signal that syncs with browser storage.
javascript
import { $ } from 'sigpro';
// localStorage (default)
const theme = $.storage('theme', 'light');
const user = $.storage('user', null);
const settings = $.storage('settings', { notifications: true });
// sessionStorage
const draft = $.storage('draft', '', sessionStorage);
const formData = $.storage('form', {}, sessionStorage);📋 API Reference
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
key | string | required | Storage key name |
initialValue | any | required | Default value if none stored |
storage | Storage | localStorage | Storage type (localStorage or sessionStorage) |
Returns
| Return | Description |
|---|---|
Function | Signal function (getter/setter) with persistence |
🎯 Basic Examples
Theme Preference
javascript
import { $, html } from 'sigpro';
// Persistent theme signal
const theme = $.storage('theme', 'light');
// Apply theme to document
$.effect(() => {
document.body.className = `theme-${theme()}`;
});
// Toggle theme
const toggleTheme = () => {
theme(t => t === 'light' ? 'dark' : 'light');
};
// Template
html`
<div>
<p>Current theme: ${theme}</p>
<button @click=${toggleTheme}>
Toggle Theme
</button>
</div>
`;User Preferences
javascript
import { $ } from 'sigpro';
// Complex preferences object
const preferences = $.storage('preferences', {
language: 'en',
fontSize: 'medium',
notifications: true,
compactView: false,
sidebarOpen: true
});
// Update single preference
const setPreference = (key, value) => {
preferences({
...preferences(),
[key]: value
});
};
// Usage
setPreference('language', 'es');
setPreference('fontSize', 'large');
console.log(preferences().language); // 'es'Form Draft
javascript
import { $, html } from 'sigpro';
// Session-based draft (clears when tab closes)
const draft = $.storage('contact-form', {
name: '',
email: '',
message: ''
}, sessionStorage);
// Auto-save on input
const handleInput = (field, value) => {
draft({
...draft(),
[field]: value
});
};
// Clear draft after submit
const handleSubmit = async () => {
await submitForm(draft());
draft(null); // Clears from storage
};
// Template
html`
<form @submit=${handleSubmit}>
<input
type="text"
:value=${() => draft().name}
@input=${(e) => handleInput('name', e.target.value)}
placeholder="Name"
/>
<input
type="email"
:value=${() => draft().email}
@input=${(e) => handleInput('email', e.target.value)}
placeholder="Email"
/>
<textarea
:value=${() => draft().message}
@input=${(e) => handleInput('message', e.target.value)}
placeholder="Message"
></textarea>
<button type="submit">Send</button>
</form>
`;🚀 Advanced Examples
Authentication State
javascript
import { $, html } from 'sigpro';
// Persistent auth state
const auth = $.storage('auth', {
token: null,
user: null,
expiresAt: null
});
// Computed helpers
const isAuthenticated = $(() => {
const { token, expiresAt } = auth();
if (!token || !expiresAt) return false;
return new Date(expiresAt) > new Date();
});
const user = $(() => auth().user);
// Login function
const login = async (email, password) => {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password })
});
if (response.ok) {
const { token, user, expiresIn } = await response.json();
auth({
token,
user,
expiresAt: new Date(Date.now() + expiresIn * 1000).toISOString()
});
return true;
}
return false;
};
// Logout
const logout = () => {
auth(null); // Clear from storage
};
// Auto-refresh token
$.effect(() => {
if (!isAuthenticated()) return;
const { expiresAt } = auth();
const expiresIn = new Date(expiresAt) - new Date();
const refreshTime = expiresIn - 60000; // 1 minute before expiry
if (refreshTime > 0) {
const timer = setTimeout(refreshToken, refreshTime);
return () => clearTimeout(timer);
}
});
// Navigation guard
$.effect(() => {
if (!isAuthenticated() && window.location.pathname !== '/login') {
$.router.go('/login');
}
});Multi-tab Synchronization
javascript
import { $ } from 'sigpro';
// Storage key for cross-tab communication
const STORAGE_KEY = 'app-state';
// Create persistent signal
const appState = $.storage(STORAGE_KEY, {
count: 0,
lastUpdated: null
});
// Listen for storage events (changes from other tabs)
window.addEventListener('storage', (event) => {
if (event.key === STORAGE_KEY && event.newValue) {
try {
// Update signal without triggering save loop
const newValue = JSON.parse(event.newValue);
appState(newValue);
} catch (e) {
console.error('Failed to parse storage event:', e);
}
}
});
// Update state (syncs across all tabs)
const increment = () => {
appState({
count: appState().count + 1,
lastUpdated: new Date().toISOString()
});
};
// Tab counter
const tabCount = $(1);
// Track number of tabs open
window.addEventListener('storage', (event) => {
if (event.key === 'tab-heartbeat') {
tabCount(parseInt(event.newValue) || 1);
}
});
// Send heartbeat
setInterval(() => {
localStorage.setItem('tab-heartbeat', tabCount());
}, 1000);Settings Manager
javascript
import { $, html } from 'sigpro';
// Settings schema
const settingsSchema = {
theme: {
type: 'select',
options: ['light', 'dark', 'system'],
default: 'system'
},
fontSize: {
type: 'range',
min: 12,
max: 24,
default: 16
},
notifications: {
type: 'checkbox',
default: true
},
language: {
type: 'select',
options: ['en', 'es', 'fr', 'de'],
default: 'en'
}
};
// Persistent settings
const settings = $.storage('app-settings',
Object.entries(settingsSchema).reduce((acc, [key, config]) => ({
...acc,
[key]: config.default
}), {})
);
// Settings component
const SettingsPanel = () => {
return html`
<div class="settings-panel">
<h2>Settings</h2>
${Object.entries(settingsSchema).map(([key, config]) => {
switch(config.type) {
case 'select':
return html`
<div class="setting">
<label>${key}:</label>
<select
:value=${() => settings()[key]}
@change=${(e) => updateSetting(key, e.target.value)}
>
${config.options.map(opt => html`
<option value="${opt}" ?selected=${() => settings()[key] === opt}>
${opt}
</option>
`)}
</select>
</div>
`;
case 'range':
return html`
<div class="setting">
<label>${key}: ${() => settings()[key]}</label>
<input
type="range"
min="${config.min}"
max="${config.max}"
:value=${() => settings()[key]}
@input=${(e) => updateSetting(key, parseInt(e.target.value))}
/>
</div>
`;
case 'checkbox':
return html`
<div class="setting">
<label>
<input
type="checkbox"
:checked=${() => settings()[key]}
@change=${(e) => updateSetting(key, e.target.checked)}
/>
${key}
</label>
</div>
`;
}
})}
<button @click=${resetDefaults}>Reset to Defaults</button>
</div>
`;
};
// Helper functions
const updateSetting = (key, value) => {
settings({
...settings(),
[key]: value
});
};
const resetDefaults = () => {
const defaults = Object.entries(settingsSchema).reduce((acc, [key, config]) => ({
...acc,
[key]: config.default
}), {});
settings(defaults);
};
// Apply settings globally
$.effect(() => {
const { theme, fontSize } = settings();
// Apply theme
document.documentElement.setAttribute('data-theme', theme);
// Apply font size
document.documentElement.style.fontSize = `${fontSize}px`;
});Shopping Cart Persistence
javascript
import { $, html } from 'sigpro';
// Persistent shopping cart
const cart = $.storage('shopping-cart', {
items: [],
lastUpdated: null
});
// Computed values
const cartItems = $(() => cart().items);
const itemCount = $(() => cartItems().reduce((sum, item) => sum + item.quantity, 0));
const subtotal = $(() => cartItems().reduce((sum, item) => sum + (item.price * item.quantity), 0));
const tax = $(() => subtotal() * 0.1);
const total = $(() => subtotal() + tax());
// Cart actions
const addToCart = (product, quantity = 1) => {
const existing = cartItems().findIndex(item => item.id === product.id);
if (existing >= 0) {
// Update quantity
const newItems = [...cartItems()];
newItems[existing] = {
...newItems[existing],
quantity: newItems[existing].quantity + quantity
};
cart({
items: newItems,
lastUpdated: new Date().toISOString()
});
} else {
// Add new item
cart({
items: [...cartItems(), { ...product, quantity }],
lastUpdated: new Date().toISOString()
});
}
};
const removeFromCart = (productId) => {
cart({
items: cartItems().filter(item => item.id !== productId),
lastUpdated: new Date().toISOString()
});
};
const updateQuantity = (productId, quantity) => {
if (quantity <= 0) {
removeFromCart(productId);
} else {
const newItems = cartItems().map(item =>
item.id === productId ? { ...item, quantity } : item
);
cart({
items: newItems,
lastUpdated: new Date().toISOString()
});
}
};
const clearCart = () => {
cart({
items: [],
lastUpdated: new Date().toISOString()
});
};
// Cart expiration (7 days)
const CART_EXPIRY_DAYS = 7;
$.effect(() => {
const lastUpdated = cart().lastUpdated;
if (lastUpdated) {
const expiryDate = new Date(lastUpdated);
expiryDate.setDate(expiryDate.getDate() + CART_EXPIRY_DAYS);
if (new Date() > expiryDate) {
clearCart();
}
}
});
// Cart display component
const CartDisplay = () => html`
<div class="cart">
<h3>Shopping Cart (${itemCount} items)</h3>
${cartItems().map(item => html`
<div class="cart-item">
<span>${item.name}</span>
<span>$${item.price} x ${item.quantity}</span>
<span>$${item.price * item.quantity}</span>
<button @click=${() => removeFromCart(item.id)}>Remove</button>
<input
type="number"
min="1"
:value=${item.quantity}
@change=${(e) => updateQuantity(item.id, parseInt(e.target.value))}
/>
</div>
`)}
<div class="cart-totals">
<p>Subtotal: $${subtotal}</p>
<p>Tax (10%): $${tax}</p>
<p><strong>Total: $${total}</strong></p>
</div>
${() => cartItems().length > 0 ? html`
<button @click=${checkout}>Checkout</button>
<button @click=${clearCart}>Clear Cart</button>
` : html`
<p>Your cart is empty</p>
`}
</div>
`;Recent Searches History
javascript
import { $, html } from 'sigpro';
// Persistent search history (max 10 items)
const searchHistory = $.storage('search-history', []);
// Add search to history
const addSearch = (query) => {
if (!query.trim()) return;
const current = searchHistory();
const newHistory = [
{ query, timestamp: new Date().toISOString() },
...current.filter(item => item.query !== query)
].slice(0, 10); // Keep only last 10
searchHistory(newHistory);
};
// Clear history
const clearHistory = () => {
searchHistory([]);
};
// Remove specific item
const removeFromHistory = (query) => {
searchHistory(searchHistory().filter(item => item.query !== query));
};
// Search component
const SearchWithHistory = () => {
const searchInput = $('');
const handleSearch = () => {
const query = searchInput();
if (query) {
addSearch(query);
performSearch(query);
searchInput('');
}
};
return html`
<div class="search-container">
<div class="search-box">
<input
type="search"
:value=${searchInput}
@keydown.enter=${handleSearch}
placeholder="Search..."
/>
<button @click=${handleSearch}>Search</button>
</div>
${() => searchHistory().length > 0 ? html`
<div class="search-history">
<h4>Recent Searches</h4>
${searchHistory().map(item => html`
<div class="history-item">
<button
class="history-query"
@click=${() => {
searchInput(item.query);
handleSearch();
}}
>
🔍 ${item.query}
</button>
<small>${new Date(item.timestamp).toLocaleString()}</small>
<button
class="remove-btn"
@click=${() => removeFromHistory(item.query)}
>
✕
</button>
</div>
`)}
<button class="clear-btn" @click=${clearHistory}>
Clear History
</button>
</div>
` : ''}
</div>
`;
};Multiple Profiles / Accounts
javascript
import { $, html } from 'sigpro';
// Profile manager
const profiles = $.storage('user-profiles', {
current: 'default',
list: {
default: {
name: 'Default',
theme: 'light',
preferences: {}
}
}
});
// Switch profile
const switchProfile = (profileId) => {
profiles({
...profiles(),
current: profileId
});
};
// Create profile
const createProfile = (name) => {
const id = `profile-${Date.now()}`;
profiles({
current: id,
list: {
...profiles().list,
[id]: {
name,
theme: 'light',
preferences: {},
createdAt: new Date().toISOString()
}
}
});
return id;
};
// Delete profile
const deleteProfile = (profileId) => {
if (profileId === 'default') return; // Can't delete default
const newList = { ...profiles().list };
delete newList[profileId];
profiles({
current: 'default',
list: newList
});
};
// Get current profile data
const currentProfile = $(() => {
const { current, list } = profiles();
return list[current] || list.default;
});
// Profile-aware settings
const profileTheme = $(() => currentProfile().theme);
const profilePreferences = $(() => currentProfile().preferences);
// Update profile data
const updateCurrentProfile = (updates) => {
const { current, list } = profiles();
profiles({
current,
list: {
...list,
[current]: {
...list[current],
...updates
}
}
});
};
// Profile selector component
const ProfileSelector = () => html`
<div class="profile-selector">
<select
:value=${() => profiles().current}
@change=${(e) => switchProfile(e.target.value)}
>
${Object.entries(profiles().list).map(([id, profile]) => html`
<option value="${id}">${profile.name}</option>
`)}
</select>
<button @click=${() => {
const name = prompt('Enter profile name:');
if (name) createProfile(name);
}}>
New Profile
</button>
</div>
`;🛡️ Error Handling
Storage Errors
javascript
import { $ } from 'sigpro';
// Safe storage wrapper
const safeStorage = (key, initialValue, storage = localStorage) => {
try {
return $.storage(key, initialValue, storage);
} catch (error) {
console.warn(`Storage failed for ${key}, using in-memory fallback:`, error);
return $(initialValue);
}
};
// Usage with fallback
const theme = safeStorage('theme', 'light');
const user = safeStorage('user', null);Quota Exceeded Handling
javascript
import { $ } from 'sigpro';
const createManagedStorage = (key, initialValue, maxSize = 1024 * 100) => { // 100KB limit
const signal = $.storage(key, initialValue);
// Monitor size
const size = $(0);
$.effect(() => {
try {
const value = signal();
const json = JSON.stringify(value);
const bytes = new Blob([json]).size;
size(bytes);
if (bytes > maxSize) {
console.warn(`Storage for ${key} exceeded ${maxSize} bytes`);
// Could implement cleanup strategy here
}
} catch (e) {
console.error('Size check failed:', e);
}
});
return { signal, size };
};
// Usage
const { signal: largeData, size } = createManagedStorage('app-data', {}, 50000);📊 Storage Limits
| Storage Type | Typical Limit | Notes |
|---|---|---|
localStorage | 5-10MB | Varies by browser |
sessionStorage | 5-10MB | Cleared when tab closes |
cookies | 4KB | Not recommended for SigPro |
🎯 Best Practices
1. Validate Stored Data
javascript
import { $ } from 'sigpro';
// Schema validation
const createValidatedStorage = (key, schema, defaultValue, storage) => {
const signal = $.storage(key, defaultValue, storage);
// Wrap to validate on read/write
const validated = (...args) => {
if (args.length) {
// Validate before writing
const value = args[0];
if (typeof value === 'function') {
// Handle functional updates
return validated(validated());
}
// Basic validation
const isValid = Object.keys(schema).every(key => {
const validator = schema[key];
return !validator || validator(value[key]);
});
if (!isValid) {
console.warn('Invalid data, skipping storage write');
return signal();
}
}
return signal(...args);
};
return validated;
};
// Usage
const userSchema = {
name: v => v && v.length > 0,
age: v => v >= 18 && v <= 120,
email: v => /@/.test(v)
};
const user = createValidatedStorage('user', userSchema, {
name: '',
age: 25,
email: ''
});2. Handle Versioning
javascript
import { $ } from 'sigpro';
const VERSION = 2;
const createVersionedStorage = (key, migrations, storage) => {
const raw = $.storage(key, { version: VERSION, data: {} }, storage);
const migrate = (data) => {
let current = data;
const currentVersion = current.version || 1;
for (let v = currentVersion; v < VERSION; v++) {
const migrator = migrations[v];
if (migrator) {
current = migrator(current);
}
}
return current;
};
// Migrate if needed
const stored = raw();
if (stored.version !== VERSION) {
const migrated = migrate(stored);
raw(migrated);
}
return raw;
};
// Usage
const migrations = {
1: (old) => ({
version: 2,
data: {
...old.data,
preferences: old.preferences || {}
}
})
};
const settings = createVersionedStorage('app-settings', migrations);3. Encrypt Sensitive Data
javascript
import { $ } from 'sigpro';
// Simple encryption (use proper crypto in production)
const encrypt = (text) => {
return btoa(text); // Base64 - NOT secure, just example
};
const decrypt = (text) => {
try {
return atob(text);
} catch {
return null;
}
};
const createSecureStorage = (key, initialValue, storage) => {
const encryptedKey = `enc_${key}`;
const signal = $.storage(encryptedKey, null, storage);
const secure = (...args) => {
if (args.length) {
// Encrypt before storing
const value = args[0];
const encrypted = encrypt(JSON.stringify(value));
return signal(encrypted);
}
// Decrypt when reading
const encrypted = signal();
if (!encrypted) return initialValue;
try {
const decrypted = decrypt(encrypted);
return decrypted ? JSON.parse(decrypted) : initialValue;
} catch {
return initialValue;
}
};
return secure;
};
// Usage
const secureToken = createSecureStorage('auth-token', null);
secureToken('sensitive-data-123'); // Stored encrypted📈 Performance Considerations
| Operation | Cost | Notes |
|---|---|---|
| Initial read | O(1) | Single storage read |
| Write | O(1) + JSON.stringify | Auto-save on change |
| Large objects | O(n) | Stringify/parse overhead |
| Multiple keys | O(k) | k = number of keys |
Pro Tip: Use
sessionStoragefor temporary data like form drafts, andlocalStoragefor persistent user preferences. Always validate data when reading from storage to handle corrupted values gracefully.