Skip to content

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 null or undefined

Storage Types

StoragePersistenceUse Case
localStorageForever (until cleared)User preferences, themes, saved data
sessionStorageUntil tab/window closesForm 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

ParameterTypeDefaultDescription
keystringrequiredStorage key name
initialValueanyrequiredDefault value if none stored
storageStoragelocalStorageStorage type (localStorage or sessionStorage)

Returns

ReturnDescription
FunctionSignal 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 TypeTypical LimitNotes
localStorage5-10MBVaries by browser
sessionStorage5-10MBCleared when tab closes
cookies4KBNot 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

OperationCostNotes
Initial readO(1)Single storage read
WriteO(1) + JSON.stringifyAuto-save on change
Large objectsO(n)Stringify/parse overhead
Multiple keysO(k)k = number of keys

Pro Tip: Use sessionStorage for temporary data like form drafts, and localStorage for persistent user preferences. Always validate data when reading from storage to handle corrupted values gracefully.