@semantq/state
Advanced tools
| // state/props.js | ||
| export function $props() { | ||
| // Your robust $props implementation goes here | ||
| console.log("Real $props logic would go here!"); | ||
| return {}; // Return something meaningful | ||
| } |
+279
| //bind.js | ||
| import { $effect } from './effect.js'; | ||
| function bindNonReactive(element, value, options = {}) { | ||
| const formattedValue = options.format ? options.format(value) : value; | ||
| if ('value' in element || element.tagName === 'SELECT') { | ||
| // Handle form elements | ||
| if (element.type === 'checkbox') { | ||
| element.checked = !!formattedValue; | ||
| } else if (element.tagName === 'SELECT' && options.multiple && Array.isArray(formattedValue)) { | ||
| Array.from(element.options).forEach(option => { | ||
| option.selected = formattedValue.includes(option.value); | ||
| }); | ||
| } else { | ||
| element.value = formattedValue ?? ''; | ||
| } | ||
| } else { | ||
| // Handle non-form elements (e.g., <p>, <span>) | ||
| element.textContent = formattedValue ?? ''; | ||
| } | ||
| } | ||
| export function bind(inputSelectorOrElement, stateOrValue, options = {}) { | ||
| const elements = getElements(inputSelectorOrElement); | ||
| if (!elements || elements.length === 0) { | ||
| console.warn(`Element(s) not found for binding: ${inputSelectorOrElement}`); | ||
| return; | ||
| } | ||
| const first = elements[0]; | ||
| const isReactive = typeof stateOrValue === 'object' && stateOrValue !== null && 'value' in stateOrValue; | ||
| // Handle radio buttons | ||
| if (first.type === 'radio') { | ||
| return bindRadioGroup(elements, stateOrValue, options, isReactive); | ||
| } | ||
| // Handle non-reactive case | ||
| if (!isReactive) { | ||
| console.log("non reactive context", first, stateOrValue, options) | ||
| bindNonReactive(first, stateOrValue, options); | ||
| return () => {}; // Return a no-op cleanup function | ||
| } | ||
| // Handle reactive case | ||
| return bindStandardElement(first, stateOrValue, options, isReactive); | ||
| } | ||
| function bindStandardElement(element, stateOrValue, options, isReactive) { | ||
| const updateElement = () => { | ||
| let value = isReactive ? stateOrValue.value : stateOrValue; | ||
| // Format the value if a formatter is provided | ||
| if (options.format) { | ||
| value = options.format(value); | ||
| } | ||
| if (element.type === 'checkbox') { | ||
| element.checked = !!value; | ||
| } else if (element.tagName === 'SELECT') { | ||
| if (options.multiple && Array.isArray(value)) { | ||
| Array.from(element.options).forEach(option => { | ||
| option.selected = value.includes(option.value); | ||
| }); | ||
| } else { | ||
| element.value = value ?? ''; | ||
| } | ||
| } else { | ||
| element.value = value ?? ''; | ||
| } | ||
| }; | ||
| // Only set up two-way binding if it's reactive | ||
| const updateState = isReactive ? () => { | ||
| let newValue; | ||
| if (element.type === 'checkbox') { | ||
| newValue = element.checked; | ||
| } else if (element.tagName === 'SELECT' && options.multiple || element.multiple) { | ||
| newValue = Array.from(element.selectedOptions).map(o => o.value); | ||
| } else { | ||
| newValue = element.value; | ||
| } | ||
| if (options.parse) { | ||
| newValue = options.parse(newValue); | ||
| } | ||
| stateOrValue.value = newValue; | ||
| } : null; | ||
| const eventType = getEventType(element); | ||
| if (updateState) { | ||
| element.addEventListener(eventType, updateState); | ||
| } | ||
| updateElement(); | ||
| // Only set up effect if it's reactive | ||
| const cleanup = isReactive ? $effect(updateElement) : null; | ||
| // Return a cleanup function for both reactive and non-reactive states | ||
| return () => { | ||
| if (updateState) { | ||
| element.removeEventListener(eventType, updateState); | ||
| } | ||
| if (cleanup) { | ||
| cleanup(); | ||
| } | ||
| }; | ||
| } | ||
| function bindRadioGroup(radios, stateOrValue, options, isReactive) { | ||
| const updateRadios = () => { | ||
| const value = isReactive ? stateOrValue.value : stateOrValue; | ||
| radios.forEach(r => { | ||
| r.checked = r.value === value; | ||
| }); | ||
| }; | ||
| // Only set up two-way binding if it's reactive | ||
| const updateState = isReactive ? (event) => { | ||
| const selected = event.target; | ||
| if (selected.checked) { | ||
| const newValue = options.parse ? options.parse(selected.value) : selected.value; | ||
| stateOrValue.value = newValue; | ||
| } | ||
| } : null; | ||
| if (updateState) { | ||
| radios.forEach(r => r.addEventListener('change', updateState)); | ||
| } | ||
| updateRadios(); | ||
| // Only set up effect if it's reactive | ||
| const cleanup = isReactive ? $effect(updateRadios) : null; | ||
| return () => { | ||
| if (updateState) { | ||
| radios.forEach(r => r.removeEventListener('change', updateState)); | ||
| } | ||
| if (cleanup) { | ||
| cleanup(); | ||
| } | ||
| }; | ||
| } | ||
| function getElements(selectorOrElement) { | ||
| if (typeof selectorOrElement === 'string') { | ||
| const found = document.querySelectorAll(selectorOrElement); | ||
| return found.length ? Array.from(found) : null; | ||
| } | ||
| return [selectorOrElement]; | ||
| } | ||
| function getEventType(element) { | ||
| if (element.type === 'checkbox' || element.tagName === 'SELECT') { | ||
| return 'change'; | ||
| } | ||
| if (element.tagName === 'INPUT' && (element.type === 'range' || element.type === 'file')) { | ||
| return 'change'; | ||
| } | ||
| return 'input'; | ||
| } | ||
| // Bind textContent to state | ||
| export function bindText(selectorOrElement, stateOrValue, options = {}) { | ||
| const element = typeof selectorOrElement === 'string' | ||
| ? document.querySelector(selectorOrElement) | ||
| : selectorOrElement; | ||
| if (!element) { | ||
| console.warn(`Element not found for text binding: ${selectorOrElement}`); | ||
| return; | ||
| } | ||
| const isReactive = typeof stateOrValue === 'object' && stateOrValue !== null && 'value' in stateOrValue; | ||
| const updateElement = () => { | ||
| const value = isReactive ? stateOrValue.value : stateOrValue; | ||
| element.textContent = options.format ? options.format(value) : value; | ||
| }; | ||
| updateElement(); | ||
| if (isReactive) { | ||
| const cleanupEffect = $effect(updateElement); | ||
| return () => cleanupEffect(); | ||
| } else { | ||
| // Handle non-reactive case | ||
| //console.log("non reactive context text", element, stateOrValue, options); | ||
| //bindNonReactive(element, stateOrValue, options); | ||
| return () => {}; // Return a no-op cleanup function | ||
| } | ||
| } | ||
| // Bind attribute | ||
| export function bindAttr(selectorOrElement, attr, state, options = {}) { | ||
| const el = typeof selectorOrElement === 'string' | ||
| ? document.querySelector(selectorOrElement) | ||
| : selectorOrElement; | ||
| if (!el) { | ||
| console.warn(`Element not found for attribute binding: ${selectorOrElement}`); | ||
| return; | ||
| } | ||
| // Check if the state is reactive (object with .value) | ||
| const isReactive = typeof state === 'object' && state !== null && 'value' in state; | ||
| const update = () => { | ||
| // Get value depending on whether it's reactive or not | ||
| const value = isReactive ? state.value : state; | ||
| el.setAttribute(attr, options.format ? options.format(value) : value); | ||
| }; | ||
| update(); | ||
| if (isReactive) { | ||
| const cleanup = $effect(update); | ||
| return () => cleanup(); // Cleanup if reactive | ||
| } else { | ||
| return () => {}; // No-op for non-reactive | ||
| } | ||
| } | ||
| // Bind class based on truthy state | ||
| export function bindClass(selectorOrElement, className, state, options = {}) { | ||
| const el = typeof selectorOrElement === 'string' | ||
| ? document.querySelector(selectorOrElement) | ||
| : selectorOrElement; | ||
| if (!el) { | ||
| console.warn(`Element not found for class binding: ${selectorOrElement}`); | ||
| return; | ||
| } | ||
| // Check if the state is reactive (object with .value) | ||
| const isReactive = typeof state === 'object' && state !== null && 'value' in state; | ||
| const update = () => { | ||
| // Get value depending on whether it's reactive or not | ||
| const value = isReactive ? state.value : state; | ||
| el.classList.toggle(className, !!value); | ||
| }; | ||
| update(); | ||
| if (isReactive) { | ||
| const cleanup = $effect(update); | ||
| return () => cleanup(); // Cleanup if reactive | ||
| } else { | ||
| return () => {}; // No-op for non-reactive | ||
| } | ||
| } | ||
| //effect.js | ||
| import { getCurrentEffect, setCurrentEffect } from './PulseCore.js'; | ||
| export function $effect(callback) { | ||
| const effect = () => { | ||
| setCurrentEffect(effect); | ||
| callback(); | ||
| setCurrentEffect(null); | ||
| }; | ||
| effect(); | ||
| return () => { | ||
| // Cleanup logic would go here | ||
| }; | ||
| } |
| // index.js | ||
| import { pulse } from './pulse.js'; | ||
| import { reState } from './reState.js'; | ||
| import { $effect } from './effect.js'; | ||
| import { bind, bindText, bindAttr, bindClass } from './bind.js'; | ||
| // Export the reactivity system | ||
| // Core primitives (direct named exports) | ||
| export { pulse as $state } from './pulse.js'; | ||
| export { reState as $derived } from './reState.js'; | ||
| export { $effect } from './effect.js'; | ||
| // Binding utilities (grouped under `state`) | ||
| export const state = { | ||
| bind, | ||
| text: bindText, | ||
| attr: bindAttr, | ||
| class: bindClass, | ||
| }; | ||
| // Optional: Re-export bind utilities directly for power users | ||
| export { bind, bindText, bindAttr, bindClass }; | ||
| // Export storage utilities | ||
| export const storage = { | ||
| get: (key) => { | ||
| try { | ||
| const stored = localStorage.getItem(key); | ||
| return stored !== null ? JSON.parse(stored) : null; | ||
| } catch (e) { | ||
| console.warn(`Failed to parse stored value for key "${key}"`, e); | ||
| return null; | ||
| } | ||
| }, | ||
| set: (key, value) => { | ||
| try { | ||
| localStorage.setItem(key, JSON.stringify(value)); | ||
| } catch (e) { | ||
| console.warn(`Failed to persist value for key "${key}"`, e); | ||
| } | ||
| }, | ||
| remove: (key) => { | ||
| localStorage.removeItem(key); | ||
| } | ||
| }; |
| //pulse | ||
| import { PulseCore } from './PulseCore.js'; | ||
| export function pulse(initialValue, options = {}) { | ||
| const { key, storage = localStorage } = options; | ||
| // Load from storage if key is provided | ||
| if (key) { | ||
| try { | ||
| const stored = storage.getItem(key); | ||
| if (stored !== null) { | ||
| initialValue = JSON.parse(stored); | ||
| } | ||
| } catch (e) { | ||
| console.warn(`Failed to parse stored value for key "${key}"`, e); | ||
| } | ||
| } | ||
| // Pass options to PulseCore to enable persistence! | ||
| const signal = new PulseCore(initialValue, { key, storage }); | ||
| return new Proxy(signal, { | ||
| get(target, prop) { | ||
| if (prop === 'value') return target.value; | ||
| if (prop === 'set') return (newValue) => { target.value = newValue; }; | ||
| return Reflect.get(target, prop); | ||
| }, | ||
| set(target, prop, value) { | ||
| if (prop === 'value') { | ||
| target.value = value; | ||
| return true; | ||
| } | ||
| return Reflect.set(target, prop, value); | ||
| } | ||
| }); | ||
| } | ||
| export const $state = pulse; |
| // PulseCore.js | ||
| let currentEffect = null; | ||
| export class PulseCore { | ||
| constructor(value, options = {}) { | ||
| this._value = value; | ||
| this._dependents = new Set(); | ||
| this._options = options; | ||
| // If persistence is enabled, set up storage listener | ||
| if (this._options.key) { | ||
| window.addEventListener('storage', this._handleStorageEvent.bind(this)); | ||
| } | ||
| } | ||
| get value() { | ||
| if (currentEffect) { | ||
| this._dependents.add(currentEffect); | ||
| } | ||
| return this._value; | ||
| } | ||
| set value(newValue) { | ||
| if (this._value !== newValue) { | ||
| this._value = newValue; | ||
| // Persist to storage if key is provided | ||
| if (this._options.key) { | ||
| try { | ||
| const storage = this._options.storage || localStorage; | ||
| storage.setItem(this._options.key, JSON.stringify(newValue)); | ||
| } catch (e) { | ||
| console.warn(`Failed to persist state for key "${this._options.key}"`, e); | ||
| } | ||
| } | ||
| const deps = new Set(this._dependents); | ||
| this._dependents.clear(); | ||
| deps.forEach(effect => effect()); | ||
| } | ||
| } | ||
| _handleStorageEvent(event) { | ||
| if (event.key === this._options.key && event.storageArea === (this._options.storage || localStorage)) { | ||
| try { | ||
| const newValue = JSON.parse(event.newValue); | ||
| if (JSON.stringify(this._value) !== event.newValue) { | ||
| this._value = newValue; | ||
| const deps = new Set(this._dependents); | ||
| this._dependents.clear(); | ||
| deps.forEach(effect => effect()); | ||
| } | ||
| } catch (e) { | ||
| console.warn(`Failed to parse stored value for key "${this._options.key}"`, e); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| export function getCurrentEffect() { | ||
| return currentEffect; | ||
| } | ||
| export function setCurrentEffect(effect) { | ||
| currentEffect = effect; | ||
| } |
| # Comprehensive @semantq/state Documentation | ||
| `@semantq/state` is a Semantq JS Framework state library. The library is however framework-agnostic state management library that packs a punch in under 5KB: | ||
| ### Highlights | ||
| ✔ **Reactivity Made Simple** – Automatic DOM updates with zero boilerplate | ||
| ✔ **Built-in Persistence** – Seamless localStorage/sessionStorage integration | ||
| ✔ **Tiny Footprint** – 3KB gzipped with zero dependencies | ||
| ✔ **Universal Binding** – Works with vanilla JS, Svelte, Vue, React, or any JS framework | ||
| ### How It Works in 30 Seconds | ||
| ```bash | ||
| npm install @semantq/state | ||
| ``` | ||
| ### Use: | ||
| ```javascript | ||
| import { $state, state } from '@semantq/state'; | ||
| // 1. Create reactive state (auto-persists to localStorage) | ||
| const cart = $state([], { key: 'user-cart' }); | ||
| // 2. Bind to DOM elements | ||
| state.bind('#checkout-form', cart); | ||
| // 3. Automatic UI updates everywhere | ||
| function addItem(item) { | ||
| cart.value = [...cart.value, item]; // Triggers DOM updates | ||
| } | ||
| ``` | ||
| **Perfect for:** | ||
| - Small to medium apps needing reactivity | ||
| - JAMstack applications | ||
| - Progressive enhancement | ||
| - Framework-agnostic libraries | ||
| ## Core Features | ||
| ### 1. State Management | ||
| ```javascript | ||
| import { $state, $derived, $effect } from '@semantq/state'; | ||
| // Reactive state | ||
| const count = $state(0); | ||
| // Computed state | ||
| const doubled = $derived(() => count.value * 2); | ||
| // Side effects | ||
| $effect(() => { | ||
| console.log(`Count is: ${count.value}`); | ||
| }); | ||
| ``` | ||
| ### 2. Storage Persistence | ||
| ```javascript | ||
| // localStorage persistence | ||
| const userPrefs = $state( | ||
| { theme: 'dark' }, | ||
| { key: 'user-preferences' } | ||
| ); | ||
| // sessionStorage persistence | ||
| const authToken = $state( | ||
| null, | ||
| { key: 'auth-token', storage: sessionStorage } | ||
| ); | ||
| ``` | ||
| ### 3. DOM Binding | ||
| #### Form Element Binding | ||
| ```javascript | ||
| import { state } from '@semantq/state'; | ||
| // Reactive form state | ||
| const formData = $state({ | ||
| username: '', | ||
| password: '', | ||
| remember: false, | ||
| plan: 'basic', | ||
| features: ['support'] | ||
| }); | ||
| // Bind form elements | ||
| state.bind('#username', formData.username); | ||
| state.bind('#password', formData.password); | ||
| state.bind('#remember', formData.remember); | ||
| state.bind('#plan', formData.plan); | ||
| state.bind('[name="features"]', formData.features, { multiple: true }); | ||
| ``` | ||
| #### Binding Types Explained | ||
| **Text Inputs:** | ||
| ```javascript | ||
| const searchQuery = $state(''); | ||
| state.bind('#search', searchQuery); | ||
| ``` | ||
| **Checkboxes:** | ||
| ```javascript | ||
| const agreeToTerms = $state(false); | ||
| state.bind('#terms-checkbox', agreeToTerms); | ||
| ``` | ||
| **Radio Groups:** | ||
| ```javascript | ||
| const paymentMethod = $state('credit'); | ||
| state.bind('[name="payment"]', paymentMethod); | ||
| ``` | ||
| **Select Elements:** | ||
| ```javascript | ||
| const country = $state('US'); | ||
| // Single select | ||
| state.bind('#country-select', country); | ||
| // Multi-select | ||
| const selectedFeatures = $state([]); | ||
| state.bind('#features-select', selectedFeatures, { multiple: true }); | ||
| ``` | ||
| **Custom Formatters/Parsers:** | ||
| ```javascript | ||
| const price = $state(0); | ||
| state.bind('#price-input', price, { | ||
| format: (value) => `$${value.toFixed(2)}`, | ||
| parse: (str) => parseFloat(str.replace(/[^0-9.]/g, '')) | ||
| }); | ||
| ``` | ||
| #### Non-Form Element Binding | ||
| **Text Content:** | ||
| ```javascript | ||
| const message = $state('Hello'); | ||
| state.text('#message-element', message); | ||
| ``` | ||
| **Attributes:** | ||
| ```javascript | ||
| const isActive = $state(true); | ||
| state.attr('#tab', 'aria-selected', isActive); | ||
| ``` | ||
| **Classes:** | ||
| ```javascript | ||
| const isDarkMode = $state(false); | ||
| state.class('#theme-toggle', 'dark-mode', isDarkMode); | ||
| ``` | ||
| ### 4. Advanced Binding Patterns | ||
| **Dynamic Element Binding:** | ||
| ```javascript | ||
| const items = $state([{ id: 1, text: 'First' }]); | ||
| $effect(() => { | ||
| items.value.forEach(item => { | ||
| state.text(`#item-${item.id}`, item.text); | ||
| }); | ||
| }); | ||
| ``` | ||
| **Form Validation:** | ||
| ```javascript | ||
| const form = $state({ email: '', password: '' }); | ||
| const errors = $derived(() => ({ | ||
| email: !form.value.email.includes('@'), | ||
| password: form.value.password.length < 8 | ||
| })); | ||
| $effect(() => { | ||
| state.class('#email-input', 'error', errors.value.email); | ||
| state.class('#password-input', 'error', errors.value.password); | ||
| }); | ||
| ``` | ||
| **Debounced Input:** | ||
| ```javascript | ||
| const searchQuery = $state('', { debounce: 300 }); | ||
| state.bind('#search-input', searchQuery); | ||
| $effect(() => { | ||
| // Will only trigger after 300ms of inactivity | ||
| fetchResults(searchQuery.value); | ||
| }); | ||
| ``` | ||
| ### 5. Storage Use Cases (Expanded) | ||
| **Authentication Flow:** | ||
| ```javascript | ||
| // auth.js | ||
| export const auth = $state( | ||
| { user: null, token: null }, | ||
| { key: 'auth', storage: sessionStorage } | ||
| ); | ||
| export function login(credentials) { | ||
| // API call would go here | ||
| auth.value = { | ||
| user: { id: 1, name: 'User' }, | ||
| token: 'abc123' | ||
| }; | ||
| } | ||
| export function logout() { | ||
| auth.value = { user: null, token: null }; | ||
| } | ||
| // Auto-logout when token expires | ||
| $effect(() => { | ||
| if (auth.value.token) { | ||
| const timer = setTimeout(logout, 3600000); | ||
| return () => clearTimeout(timer); | ||
| } | ||
| }); | ||
| ``` | ||
| **E-commerce Cart:** | ||
| ```javascript | ||
| // cart.js | ||
| export const cart = $state( | ||
| { items: [], lastUpdated: null }, | ||
| { key: 'cart', debounce: 500 } | ||
| ); | ||
| export function addToCart(product, quantity = 1) { | ||
| cart.value = { | ||
| items: [ | ||
| ...cart.value.items.filter(item => item.id !== product.id), | ||
| { ...product, quantity } | ||
| ], | ||
| lastUpdated: new Date().toISOString() | ||
| }; | ||
| } | ||
| // Persist cart for 7 days | ||
| $effect(() => { | ||
| if (cart.value.lastUpdated) { | ||
| const weekOld = new Date(); | ||
| weekOld.setDate(weekOld.getDate() - 7); | ||
| if (new Date(cart.value.lastUpdated) < weekOld) { | ||
| cart.value = { items: [], lastUpdated: null }; | ||
| } | ||
| } | ||
| }); | ||
| ``` | ||
| ### 6. Framework Integration | ||
| **React Example:** | ||
| ```javascript | ||
| import { $state, $effect } from '@semantq/state'; | ||
| import { useEffect } from 'react'; | ||
| function Counter() { | ||
| const count = $state(0); | ||
| // Sync state with React | ||
| const [reactCount, setReactCount] = useState(count.value); | ||
| $effect(() => setReactCount(count.value)); | ||
| return ( | ||
| <div> | ||
| <button onClick={() => count.value--}>-</button> | ||
| <span>{reactCount}</span> | ||
| <button onClick={() => count.value++}>+</button> | ||
| </div> | ||
| ); | ||
| } | ||
| ``` | ||
| **Vue Example:** | ||
| ```javascript | ||
| import { $state } from '@semantq/state'; | ||
| export default { | ||
| setup() { | ||
| const count = $state(0); | ||
| return { count }; | ||
| }, | ||
| template: ` | ||
| <div> | ||
| <button @click="count.value--">-</button> | ||
| {{ count.value }} | ||
| <button @click="count.value++">+</button> | ||
| </div> | ||
| ` | ||
| }; | ||
| ``` | ||
| ### 7. Performance Optimization | ||
| **Batch Updates:** | ||
| ```javascript | ||
| const user = $state({ name: '', email: '' }); | ||
| // Instead of: | ||
| user.value.name = 'John'; | ||
| user.value.email = 'john@example.com'; | ||
| // Do: | ||
| user.value = { ...user.value, name: 'John', email: 'john@example.com' }; | ||
| ``` | ||
| **Selective Binding:** | ||
| ```javascript | ||
| const largeData = $state(/* ... */); | ||
| // Bind only needed properties | ||
| state.bind('#name-display', $derived(() => largeData.value.user.name)); | ||
| ``` | ||
| ### 8. Security Best Practices | ||
| **Sensitive Data Handling:** | ||
| ```javascript | ||
| // auth.js | ||
| export const auth = $state( | ||
| { | ||
| // Only store minimal necessary auth data | ||
| userId: '123', | ||
| token: null, // JWT token (short-lived) | ||
| refreshToken: null // Only in memory | ||
| }, | ||
| { | ||
| key: 'auth', | ||
| storage: sessionStorage, | ||
| // Optional encryption for extra security | ||
| transform: { | ||
| serialize: (value) => encrypt(JSON.stringify(value)), | ||
| deserialize: (str) => JSON.parse(decrypt(str)) | ||
| } | ||
| } | ||
| ); | ||
| ``` | ||
| ### 9. Migration Strategies | ||
| **Versioned State:** | ||
| ```javascript | ||
| const APP_VERSION = '1.0'; | ||
| export const settings = $state( | ||
| { version: APP_VERSION, theme: 'light' }, | ||
| { | ||
| key: 'settings', | ||
| migrate: (oldValue) => { | ||
| if (!oldValue.version || oldValue.version !== APP_VERSION) { | ||
| // Return default/migrated state | ||
| return { version: APP_VERSION, theme: 'light' }; | ||
| } | ||
| return oldValue; | ||
| } | ||
| } | ||
| ); | ||
| ``` | ||
| ### 10. Testing Patterns | ||
| **Mocking Storage:** | ||
| ```javascript | ||
| // test-utils.js | ||
| export function createMockStorage() { | ||
| let store = {}; | ||
| return { | ||
| getItem: (key) => store[key], | ||
| setItem: (key, value) => { store[key] = value; }, | ||
| removeItem: (key) => { delete store[key]; }, | ||
| clear: () => { store = {}; } | ||
| }; | ||
| } | ||
| // In your tests: | ||
| const mockStorage = createMockStorage(); | ||
| const testState = $state(0, { key: 'test', storage: mockStorage }); | ||
| ``` | ||
| ## Complete API Reference | ||
| ### Core API | ||
| | Function | Description | | ||
| |----------------|-----------------------------------------------------------------------------| | ||
| | `$state(value, options?)` | Creates a reactive state container | | ||
| | `$derived(fn)` | Creates a computed value derived from other states | | ||
| | `$effect(fn)` | Runs side effects when dependencies change | | ||
| ### Binding API | ||
| | Method | Description | | ||
| |---------------------------------|-----------------------------------------------------------------------------| | ||
| | `state.bind(selector, state, options?)` | Two-way binding for form elements | | ||
| | `state.text(selector, state)` | One-way binding for text content | | ||
| | `state.attr(selector, attr, state)` | One-way binding for attributes | | ||
| | `state.class(selector, className, state)` | Toggles classes based on boolean state | | ||
| ### Storage API | ||
| | Method | Description | | ||
| |---------------------------|-----------------------------------------------------------------------------| | ||
| | `storage.get(key)` | Retrieves a value from storage | | ||
| | `storage.set(key, value)` | Stores a value in storage | | ||
| | `storage.remove(key)` | Removes a value from storage | | ||
| ## Troubleshooting Guide | ||
| **Binding Not Working:** | ||
| 1. Verify element exists when binding is called | ||
| 2. Check for console errors | ||
| 3. Ensure state is reactive (`$state` or `$derived`) | ||
| **Storage Issues:** | ||
| 1. Check browser storage limits | ||
| 2. Verify storage is not disabled (private mode) | ||
| 3. Ensure data is JSON-serializable | ||
| **Performance Problems:** | ||
| 1. Add debounce to frequent updates | ||
| 2. Batch multiple state updates | ||
| 3. Use derived state for computations | ||
| ## Migration from Other Libraries | ||
| **Redux/Pinia:** | ||
| - Replace slices with individual states | ||
| - Use derived state instead of selectors | ||
| - Effects replace middleware | ||
| **MobX:** | ||
| - `$state` replaces `observable` | ||
| - `$derived` replaces `computed` | ||
| - `$effect` replaces `autorun` | ||
| ## Final Recommendations | ||
| 1. **Start Simple**: Begin with basic state, add persistence as needed | ||
| 2. **Organize by Feature**: Group related states together | ||
| 3. **Monitor Storage**: Regularly check what you're persisting | ||
| 4. **Test Thoroughly**: Especially edge cases around persistence | ||
| This comprehensive approach to state management with @semantq/state provides a flexible, powerful solution that works across any JavaScript framework while maintaining excellent performance and security characteristics. |
| // reState.js | ||
| import { PulseCore, getCurrentEffect, setCurrentEffect } from './PulseCore.js'; | ||
| export function reState(computation) { | ||
| const effect = () => { | ||
| setCurrentEffect(effect); | ||
| const newValue = computation(); | ||
| setCurrentEffect(null); | ||
| if (result._value !== newValue) { | ||
| result._value = newValue; | ||
| const deps = new Set(result._dependents); | ||
| result._dependents.clear(); | ||
| deps.forEach(dep => dep()); | ||
| } | ||
| }; | ||
| const result = new PulseCore(undefined); // Changed to PulseCore | ||
| effect(); | ||
| return new Proxy(result, { | ||
| get(target, prop) { | ||
| if (prop === 'value') return target.value; | ||
| return Reflect.get(target, prop); | ||
| } | ||
| }); | ||
| } | ||
| export const $derived = reState; |
+1
-0
@@ -0,1 +1,2 @@ | ||
| //bind.js | ||
| import { $effect } from './effect.js'; | ||
@@ -2,0 +3,0 @@ |
+3
-0
@@ -6,3 +6,5 @@ // index.js | ||
| import { bind, bindText, bindAttr, bindClass } from './bind.js'; | ||
| import { $props } from './props.js'; | ||
| // Export the reactivity system | ||
@@ -13,2 +15,3 @@ // Core primitives (direct named exports) | ||
| export { $effect } from './effect.js'; | ||
| export { $props }; | ||
@@ -15,0 +18,0 @@ // Binding utilities (grouped under `state`) |
+1
-0
@@ -0,1 +1,2 @@ | ||
| //pulse | ||
| import { PulseCore } from './PulseCore.js'; | ||
@@ -2,0 +3,0 @@ |
+33
-11
@@ -7,7 +7,9 @@ // PulseCore.js | ||
| this._value = value; | ||
| // Keep _dependents as a Set of functions (the effects) | ||
| this._dependents = new Set(); | ||
| this._options = options; | ||
| // If persistence is enabled, set up storage listener | ||
| if (this._options.key) { | ||
| this._loadFromStorage(); // Load initial value if persistence enabled | ||
| window.addEventListener('storage', this._handleStorageEvent.bind(this)); | ||
@@ -17,4 +19,20 @@ } | ||
| _loadFromStorage() { | ||
| if (this._options.key) { | ||
| try { | ||
| const storage = this._options.storage || localStorage; | ||
| const stored = storage.getItem(this._options.key); | ||
| if (stored !== null) { | ||
| this._value = JSON.parse(stored); | ||
| } | ||
| } catch (e) { | ||
| console.warn(`Failed to load state for key "${this._options.key}" from storage`, e); | ||
| } | ||
| } | ||
| } | ||
| get value() { | ||
| if (currentEffect) { | ||
| // Add the current effect to this PulseCore's dependents | ||
| // The effect is responsible for clearing its *own* old dependencies | ||
| this._dependents.add(currentEffect); | ||
@@ -26,5 +44,6 @@ } | ||
| set value(newValue) { | ||
| // Only proceed if the value has actually changed | ||
| if (this._value !== newValue) { | ||
| this._value = newValue; | ||
| // Persist to storage if key is provided | ||
@@ -39,9 +58,11 @@ if (this._options.key) { | ||
| } | ||
| const deps = new Set(this._dependents); | ||
| this._dependents.clear(); | ||
| deps.forEach(effect => effect()); | ||
| // Notify all subscribed effects to re-run | ||
| // Create a new array from the Set to prevent issues if effects modify the Set during iteration | ||
| [...this._dependents].forEach(effect => effect()); | ||
| // IMPORTANT: Do NOT clear this._dependents here. | ||
| // Dependents will re-add themselves when their effect re-runs and reads this value. | ||
| } | ||
| } | ||
| _handleStorageEvent(event) { | ||
@@ -51,7 +72,8 @@ if (event.key === this._options.key && event.storageArea === (this._options.storage || localStorage)) { | ||
| const newValue = JSON.parse(event.newValue); | ||
| if (JSON.stringify(this._value) !== event.newValue) { | ||
| // Only update if the value from storage is different to avoid unnecessary notifications | ||
| if (JSON.stringify(this._value) !== event.newValue) { // Compare stringified to handle complex objects | ||
| this._value = newValue; | ||
| const deps = new Set(this._dependents); | ||
| this._dependents.clear(); | ||
| deps.forEach(effect => effect()); | ||
| // Notify internal dependents (effects) that this value has changed | ||
| // This is important for cross-tab or cross-window sync | ||
| [...this._dependents].forEach(effect => effect()); | ||
| } | ||
@@ -58,0 +80,0 @@ } catch (e) { |
+35
-20
| // reState.js | ||
| import { PulseCore, getCurrentEffect, setCurrentEffect } from './PulseCore.js'; | ||
| // No need to import getCurrentEffect, setCurrentEffect here directly, | ||
| // as $effect (from effect.js) will handle that. | ||
| import { PulseCore } from './PulseCore.js'; | ||
| import { $effect } from './effect.js'; // Ensure this path is correct relative to reState.js | ||
| /** | ||
| * Creates a reactive derived value. | ||
| * Its value is computed from other reactive states ($state) or other derived values ($derived). | ||
| * It automatically recomputes when its dependencies change and notifies its own subscribers. | ||
| * @param {Function} computation A function that computes the derived value. This function | ||
| * should read from other reactive states/deriveds. | ||
| * @returns {PulseCore} A PulseCore instance that holds the derived value. | ||
| */ | ||
| export function reState(computation) { | ||
| const effect = () => { | ||
| setCurrentEffect(effect); | ||
| // 1. Create the PulseCore instance to hold the derived value. | ||
| // This `derivedPulse` instance will also manage its own dependents ($effect blocks | ||
| // that read this derived value). | ||
| const derivedPulse = new PulseCore(undefined); // Initialize with undefined, its value will be set by computation() | ||
| // 2. Wrap the `computation` function inside an $effect. | ||
| // This internal $effect will automatically track the dependencies (other $state or $derived | ||
| // values) that are accessed when `computation` is executed. | ||
| $effect(() => { | ||
| // When this internal effect runs, it recomputes the derived value. | ||
| const newValue = computation(); | ||
| setCurrentEffect(null); | ||
| if (result._value !== newValue) { | ||
| result._value = newValue; | ||
| const deps = new Set(result._dependents); | ||
| result._dependents.clear(); | ||
| deps.forEach(dep => dep()); | ||
| // 3. Update the value of the `derivedPulse` ONLY IF it has changed. | ||
| // Importantly, setting `derivedPulse.value` will trigger the setter logic | ||
| // in `PulseCore`, which then notifies *its own* subscribers (the $effect blocks | ||
| // that depend on this derived value, like your if_condition_X variables). | ||
| // This is crucial for propagating the change. | ||
| if (derivedPulse.value !== newValue) { | ||
| derivedPulse.value = newValue; | ||
| } | ||
| }; | ||
| const result = new PulseCore(undefined); // Changed from Signal to PulseCore | ||
| effect(); | ||
| return new Proxy(result, { | ||
| get(target, prop) { | ||
| if (prop === 'value') return target.value; | ||
| return Reflect.get(target, prop); | ||
| } | ||
| }); | ||
| // Return the PulseCore instance directly. | ||
| // It already has a `value` getter/setter, so a Proxy is not needed for basic access. | ||
| return derivedPulse; | ||
| } | ||
| // Export as $derived for use in your transpiled code | ||
| export const $derived = reState; |
+2
-2
| { | ||
| "name": "@semantq/state", | ||
| "version": "1.0.5", | ||
| "description": "A reactive state management system for Semantq", | ||
| "version": "1.0.6", | ||
| "description": "A reactivity state management system for Semantq", | ||
| "main": "./core/index.js", | ||
@@ -6,0 +6,0 @@ "type": "module", |
54080
106.23%17
88.89%797
112.53%