# Effects API ๐Ÿ”„ Effects are the bridge between reactive signals and side effects in your application. They automatically track signal dependencies and re-run whenever those signals change, enabling everything from DOM updates to data fetching and localStorage synchronization. ## Core Concepts ### What is an Effect? An effect is a function that: - **Runs immediately** when created - **Tracks all signals** read during its execution - **Re-runs automatically** when any tracked signal changes - **Can return a cleanup function** that runs before the next execution or when the effect is stopped ### How Effects Work 1. When an effect runs, it sets itself as the `activeEffect` 2. Any signal read during execution adds the effect to its subscribers 3. When a signal changes, it queues all its subscribers 4. Effects are batched and run in the next microtask 5. If an effect returns a function, it's stored as a cleanup handler ## `$.effect(effectFn)` Creates a reactive effect that automatically tracks dependencies and re-runs when they change. ```javascript import { $ } from 'sigpro'; const count = $(0); $.effect(() => { console.log(`Count is: ${count()}`); }); // Logs: "Count is: 0" count(1); // Logs: "Count is: 1" ``` ## ๐Ÿ“‹ API Reference | Pattern | Example | Description | |---------|---------|-------------| | Basic Effect | `$.effect(() => console.log(count()))` | Run on dependency changes | | With Cleanup | `$.effect(() => { timer = setInterval(...); return () => clearInterval(timer) })` | Return cleanup function | | Stop Effect | `const stop = $.effect(...); stop()` | Manually stop an effect | ### Effect Object (Internal) | Property/Method | Description | |-----------------|-------------| | `dependencies` | Set of signal subscriber sets this effect belongs to | | `cleanupHandlers` | Set of cleanup functions to run before next execution | | `run()` | Executes the effect and tracks dependencies | | `stop()` | Stops the effect and runs all cleanup handlers | ## ๐ŸŽฏ Basic Examples ### Console Logging ```javascript import { $ } from 'sigpro'; const name = $('World'); const count = $(0); $.effect(() => { console.log(`Hello ${name()}! Count is ${count()}`); }); // Logs: "Hello World! Count is 0" name('John'); // Logs: "Hello John! Count is 0" count(5); // Logs: "Hello John! Count is 5" ``` ### DOM Updates ```javascript import { $ } from 'sigpro'; const count = $(0); const element = document.getElementById('counter'); $.effect(() => { element.textContent = `Count: ${count()}`; }); // Updates DOM automatically when count changes count(10); // Element text becomes "Count: 10" ``` ### Document Title ```javascript import { $ } from 'sigpro'; const page = $('home'); const unreadCount = $(0); $.effect(() => { const base = page() === 'home' ? 'Home' : 'Dashboard'; const unread = unreadCount() > 0 ? ` (${unreadCount()})` : ''; document.title = `${base}${unread} - My App`; }); page('dashboard'); // Title: "Dashboard - My App" unreadCount(3); // Title: "Dashboard (3) - My App" ``` ## ๐Ÿงน Effects with Cleanup Cleanup functions are essential for managing resources like intervals, event listeners, and subscriptions. ### Basic Cleanup ```javascript import { $ } from 'sigpro'; const userId = $(1); $.effect(() => { const id = userId(); console.log(`Setting up timer for user ${id}`); const timer = setInterval(() => { console.log(`Polling user ${id}...`); }, 1000); // Cleanup runs before next effect execution return () => { console.log(`Cleaning up timer for user ${id}`); clearInterval(timer); }; }); // Sets up timer for user 1 userId(2); // Cleans up timer for user 1 // Sets up timer for user 2 ``` ### Event Listener Cleanup ```javascript import { $ } from 'sigpro'; const isListening = $(false); $.effect(() => { if (!isListening()) return; const handleClick = (e) => { console.log('Window clicked:', e.clientX, e.clientY); }; window.addEventListener('click', handleClick); console.log('Click listener added'); return () => { window.removeEventListener('click', handleClick); console.log('Click listener removed'); }; }); isListening(true); // Adds listener isListening(false); // Removes listener isListening(true); // Adds listener again ``` ### WebSocket Connection ```javascript import { $ } from 'sigpro'; const room = $('general'); const messages = $([]); $.effect(() => { const currentRoom = room(); console.log(`Connecting to room: ${currentRoom}`); const ws = new WebSocket(`wss://chat.example.com/${currentRoom}`); ws.onmessage = (event) => { messages([...messages(), JSON.parse(event.data)]); }; ws.onerror = (error) => { console.error('WebSocket error:', error); }; // Cleanup: close connection when room changes return () => { console.log(`Disconnecting from room: ${currentRoom}`); ws.close(); }; }); room('random'); // Closes 'general' connection, opens 'random' ``` ## โฑ๏ธ Effect Timing and Batching ### Microtask Batching Effects are batched using `queueMicrotask` for optimal performance: ```javascript import { $ } from 'sigpro'; const a = $(1); const b = $(2); const c = $(3); $.effect(() => { console.log('Effect ran with:', a(), b(), c()); }); // Logs immediately: "Effect ran with: 1 2 3" // Multiple updates in same tick - only one effect run! a(10); b(20); c(30); // Only logs once: "Effect ran with: 10 20 30" ``` ### Async Effects Effects can be asynchronous, but be careful with dependency tracking: ```javascript import { $ } from 'sigpro'; const userId = $(1); const userData = $(null); $.effect(() => { const id = userId(); console.log(`Fetching user ${id}...`); // Only id() is tracked (synchronous part) fetch(`/api/users/${id}`) .then(res => res.json()) .then(data => { // This runs later - no dependency tracking here! userData(data); }); }); userId(2); // Triggers effect again, cancels previous fetch ``` ### Effect with AbortController For proper async cleanup with fetch: ```javascript import { $ } from 'sigpro'; const userId = $(1); const userData = $(null); const loading = $(false); $.effect(() => { const id = userId(); const controller = new AbortController(); loading(true); fetch(`/api/users/${id}`, { signal: controller.signal }) .then(res => res.json()) .then(data => { userData(data); loading(false); }) .catch(err => { if (err.name !== 'AbortError') { console.error('Fetch error:', err); loading(false); } }); // Cleanup: abort fetch if userId changes before completion return () => { controller.abort(); }; }); ``` ## ๐ŸŽจ Advanced Effect Patterns ### Debounced Effects ```javascript import { $ } from 'sigpro'; const searchTerm = $(''); const results = $([]); let debounceTimeout; $.effect(() => { const term = searchTerm(); // Clear previous timeout clearTimeout(debounceTimeout); // Don't search if term is too short if (term.length < 3) { results([]); return; } // Debounce search debounceTimeout = setTimeout(async () => { console.log('Searching for:', term); const data = await fetch(`/api/search?q=${term}`).then(r => r.json()); results(data); }, 300); // Cleanup on effect re-run return () => clearTimeout(debounceTimeout); }); ``` ### Throttled Effects ```javascript import { $ } from 'sigpro'; const scrollPosition = $(0); let lastRun = 0; let rafId = null; $.effect(() => { const pos = scrollPosition(); // Throttle with requestAnimationFrame if (rafId) cancelAnimationFrame(rafId); rafId = requestAnimationFrame(() => { console.log('Scroll position:', pos); updateScrollUI(pos); lastRun = Date.now(); rafId = null; }); return () => { if (rafId) { cancelAnimationFrame(rafId); rafId = null; } }; }); // Even with many updates, effect runs at most once per frame for (let i = 0; i < 100; i++) { scrollPosition(i); } ``` ### Conditional Effects ```javascript import { $ } from 'sigpro'; const isEnabled = $(false); const value = $(0); const threshold = $(10); $.effect(() => { // Effect only runs when isEnabled is true if (!isEnabled()) return; console.log(`Monitoring value: ${value()}, threshold: ${threshold()}`); if (value() > threshold()) { alert(`Value ${value()} exceeded threshold ${threshold()}!`); } }); isEnabled(true); // Effect starts monitoring value(15); // Triggers alert isEnabled(false); // Effect stops (still runs, but condition prevents logic) ``` ### Effect with Multiple Cleanups ```javascript import { $ } from 'sigpro'; const config = $({ theme: 'light', notifications: true }); $.effect(() => { const { theme, notifications } = config(); const cleanups = []; // Setup theme document.body.className = `theme-${theme}`; cleanups.push(() => { document.body.classList.remove(`theme-${theme}`); }); // Setup notifications if (notifications) { const handler = (e) => console.log('Notification:', e.detail); window.addEventListener('notification', handler); cleanups.push(() => { window.removeEventListener('notification', handler); }); } // Return combined cleanup return () => { cleanups.forEach(cleanup => cleanup()); }; }); ``` ## ๐ŸŽฏ Effects in Components ### Component Lifecycle ```javascript import { $, html } from 'sigpro'; $.component('timer-display', () => { const seconds = $(0); // Effect for timer - automatically cleaned up when component unmounts $.effect(() => { const interval = setInterval(() => { seconds(s => s + 1); }, 1000); return () => clearInterval(interval); }); return html`

Timer: ${seconds}s

`; }); ``` ### Effects with Props ```javascript import { $, html } from 'sigpro'; $.component('data-viewer', (props) => { const data = $(null); const error = $(null); // Effect reacts to prop changes $.effect(() => { const url = props.url(); if (!url) return; const controller = new AbortController(); fetch(url, { signal: controller.signal }) .then(res => res.json()) .then(data) .catch(err => { if (err.name !== 'AbortError') { error(err.message); } }); return () => controller.abort(); }); return html`
${() => { if (error()) return html`
${error()}
`; if (!data()) return html`
Loading...
`; return html`
${JSON.stringify(data(), null, 2)}
`; }}
`; }, ['url']); ``` ## ๐Ÿ”ง Effect Management ### Stopping Effects ```javascript import { $ } from 'sigpro'; const count = $(0); // Start effect const stopEffect = $.effect(() => { console.log('Count:', count()); }); count(1); // Logs: "Count: 1" count(2); // Logs: "Count: 2" // Stop the effect stopEffect(); count(3); // No logging - effect is stopped ``` ### Conditional Effect Stopping ```javascript import { $ } from 'sigpro'; const isActive = $(true); const count = $(0); let currentEffect = null; $.effect(() => { if (isActive()) { // Start or restart the monitoring effect if (currentEffect) currentEffect(); currentEffect = $.effect(() => { console.log('Monitoring count:', count()); }); } else { // Stop monitoring if (currentEffect) { currentEffect(); currentEffect = null; } } }); ``` ### Nested Effects ```javascript import { $ } from 'sigpro'; const user = $({ id: 1, name: 'John' }); const settings = $({ theme: 'dark' }); $.effect(() => { console.log('User changed:', user().name); // Nested effect - tracks settings independently $.effect(() => { console.log('Settings changed:', settings().theme); }); // When user changes, the nested effect is recreated }); ``` ## ๐Ÿš€ Real-World Examples ### Auto-saving Form ```javascript import { $ } from 'sigpro'; const formData = $({ title: '', content: '', tags: [] }); const lastSaved = $(null); const saveStatus = $('idle'); // 'idle', 'saving', 'saved', 'error' let saveTimeout; $.effect(() => { const data = formData(); // Clear previous timeout clearTimeout(saveTimeout); // Don't save empty form if (!data.title && !data.content) { saveStatus('idle'); return; } saveStatus('saving'); // Debounce save saveTimeout = setTimeout(async () => { try { await fetch('/api/posts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); saveStatus('saved'); lastSaved(new Date()); } catch (error) { saveStatus('error'); console.error('Auto-save failed:', error); } }, 1000); return () => clearTimeout(saveTimeout); }); // UI feedback const statusMessage = $(() => { const status = saveStatus(); const saved = lastSaved(); if (status === 'saving') return 'Saving...'; if (status === 'error') return 'Save failed'; if (status === 'saved' && saved) { return `Last saved: ${saved().toLocaleTimeString()}`; } return ''; }); ``` ### Real-time Search with Debounce ```javascript import { $ } from 'sigpro'; const searchInput = $(''); const searchResults = $([]); const searchStatus = $('idle'); // 'idle', 'searching', 'results', 'no-results', 'error' let searchTimeout; let abortController = null; $.effect(() => { const query = searchInput().trim(); // Clear previous timeout clearTimeout(searchTimeout); // Cancel previous request if (abortController) { abortController.abort(); abortController = null; } // Don't search for short queries if (query.length < 2) { searchResults([]); searchStatus('idle'); return; } searchStatus('searching'); // Debounce search searchTimeout = setTimeout(async () => { abortController = new AbortController(); try { const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal: abortController.signal }); const data = await response.json(); if (!abortController.signal.aborted) { searchResults(data); searchStatus(data.length ? 'results' : 'no-results'); abortController = null; } } catch (error) { if (error.name !== 'AbortError') { console.error('Search failed:', error); searchStatus('error'); } } }, 300); return () => { clearTimeout(searchTimeout); if (abortController) { abortController.abort(); abortController = null; } }; }); ``` ### Analytics Tracking ```javascript import { $ } from 'sigpro'; // Analytics configuration const analyticsEnabled = $(true); const currentPage = $('/'); const userProperties = $({}); // Track page views $.effect(() => { if (!analyticsEnabled()) return; const page = currentPage(); const properties = userProperties(); console.log('Track page view:', page, properties); // Send to analytics gtag('config', 'GA-MEASUREMENT-ID', { page_path: page, ...properties }); }); // Track user interactions const trackEvent = (eventName, properties = {}) => { $.effect(() => { if (!analyticsEnabled()) return; console.log('Track event:', eventName, properties); gtag('event', eventName, properties); }); }; // Usage currentPage('/dashboard'); userProperties({ userId: 123, plan: 'premium' }); trackEvent('button_click', { buttonId: 'signup' }); ``` ### Keyboard Shortcuts ```javascript import { $ } from 'sigpro'; const shortcuts = $({ 'ctrl+s': { handler: null, description: 'Save' }, 'ctrl+z': { handler: null, description: 'Undo' }, 'ctrl+shift+z': { handler: null, description: 'Redo' }, 'escape': { handler: null, description: 'Close modal' } }); const pressedKeys = new Set(); $.effect(() => { const handleKeyDown = (e) => { const key = e.key.toLowerCase(); const ctrl = e.ctrlKey ? 'ctrl+' : ''; const shift = e.shiftKey ? 'shift+' : ''; const alt = e.altKey ? 'alt+' : ''; const meta = e.metaKey ? 'meta+' : ''; const combo = `${ctrl}${shift}${alt}${meta}${key}`.replace(/\+$/, ''); const shortcut = shortcuts()[combo]; if (shortcut?.handler) { e.preventDefault(); shortcut.handler(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }); // Register shortcuts shortcuts({ ...shortcuts(), 'ctrl+s': { handler: () => saveDocument(), description: 'Save document' }, 'ctrl+z': { handler: () => undo(), description: 'Undo' } }); ``` ### Infinite Scroll ```javascript import { $ } from 'sigpro'; const posts = $([]); const page = $(1); const hasMore = $(true); const loading = $(false); let observer = null; // Load more posts const loadMore = async () => { if (loading() || !hasMore()) return; loading(true); try { const response = await fetch(`/api/posts?page=${page()}`); const newPosts = await response.json(); if (newPosts.length === 0) { hasMore(false); } else { posts([...posts(), ...newPosts]); page(p => p + 1); } } finally { loading(false); } }; // Setup intersection observer for infinite scroll $.effect(() => { const sentinel = document.getElementById('sentinel'); if (!sentinel) return; observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting && !loading() && hasMore()) { loadMore(); } }, { threshold: 0.1 } ); observer.observe(sentinel); return () => { if (observer) { observer.disconnect(); observer = null; } }; }); // Initial load loadMore(); ``` ## ๐Ÿ“Š Performance Considerations | Pattern | Performance Impact | Best Practice | |---------|-------------------|---------------| | Multiple signal reads | O(n) per effect | Group related signals | | Deep object access | Minimal | Use computed signals | | Large arrays | O(n) for iteration | Memoize with computed | | Frequent updates | Batched | Let batching work | | Heavy computations | Blocking | Use Web Workers | ## ๐ŸŽฏ Best Practices ### 1. Keep Effects Focused ```javascript // โŒ Avoid doing too much in one effect $.effect(() => { updateUI(count()); // UI update saveToStorage(count()); // Storage sendAnalytics(count()); // Analytics validate(count()); // Validation }); // โœ… Split into focused effects $.effect(() => updateUI(count())); $.effect(() => saveToStorage(count())); $.effect(() => sendAnalytics(count())); $.effect(() => validate(count())); ``` ### 2. Always Clean Up ```javascript // โŒ Missing cleanup $.effect(() => { const timer = setInterval(() => {}, 1000); // Memory leak! }); // โœ… Proper cleanup $.effect(() => { const timer = setInterval(() => {}, 1000); return () => clearInterval(timer); }); ``` ### 3. Avoid Writing to Signals in Effects ```javascript import { $ } from 'sigpro'; const a = $(1); const b = $(2); // โŒ Avoid - can cause loops $.effect(() => { a(b()); // Writing to a while reading b }); // โœ… Use computed signals instead const sum = $(() => a() + b()); ``` ### 4. Use Conditional Logic Carefully ```javascript // โŒ Condition affects dependency tracking $.effect(() => { if (condition()) { console.log(a()); // Only tracks a when condition is true } }); // โœ… Track all dependencies explicitly $.effect(() => { const cond = condition(); // Track condition if (cond) { console.log(a()); // Track a } }); ``` ### 5. Memoize Expensive Computations ```javascript import { $ } from 'sigpro'; const items = $([]); // โŒ Expensive computation runs on every effect $.effect(() => { const total = items().reduce((sum, i) => sum + i.price, 0); updateTotal(total); }); // โœ… Memoize with computed signal const total = $(() => items().reduce((sum, i) => sum + i.price, 0)); $.effect(() => updateTotal(total())); ``` ## ๐Ÿ” Debugging Effects ### Logging Effect Runs ```javascript import { $ } from 'sigpro'; const withLogging = (effectFn, name) => { return $.effect(() => { console.log(`[${name}] Running...`); const start = performance.now(); const result = effectFn(); const duration = performance.now() - start; console.log(`[${name}] Completed in ${duration.toFixed(2)}ms`); return result; }); }; // Usage withLogging(() => { console.log('Count:', count()); }, 'count-effect'); ``` ### Effect Inspector ```javascript import { $ } from 'sigpro'; const createEffectInspector = () => { const effects = new Map(); let id = 0; const trackedEffect = (fn, name = `effect-${++id}`) => { const info = { name, runs: 0, lastRun: null, duration: 0, dependencies: new Set() }; const wrapped = () => { info.runs++; info.lastRun = new Date(); const start = performance.now(); const result = fn(); info.duration = performance.now() - start; return result; }; const stop = $.effect(wrapped); effects.set(stop, info); return stop; }; const getReport = () => { const report = {}; effects.forEach((info, stop) => { report[info.name] = { runs: info.runs, lastRun: info.lastRun, avgDuration: info.duration / info.runs }; }); return report; }; return { trackedEffect, getReport }; }; // Usage const inspector = createEffectInspector(); inspector.trackedEffect(() => { console.log('Count:', count()); }, 'counter-effect'); ``` ## ๐Ÿ“Š Summary | Feature | Description | |---------|-------------| | **Automatic Tracking** | Dependencies tracked automatically | | **Cleanup Functions** | Return function to clean up resources | | **Batch Updates** | Multiple changes batched in microtask | | **Manual Stop** | Can stop effects with returned function | | **Nested Effects** | Effects can contain other effects | | **Auto-cleanup** | Effects in pages/components auto-cleaned | --- > **Pro Tip:** Effects are the perfect place for side effects like DOM updates, data fetching, and subscriptions. Keep them focused and always clean up resources!