a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt/
hypermedia frontend signals
1# Reactivity 2 3VoltX uses signal-based reactivity for state management. State changes automatically trigger DOM updates without virtual DOM diffing or reconciliation. 4 5## Reactive Primitives 6 7### Signals 8 9Signals are the foundation of reactive state. 10A signal holds a single value that can be read, written, and observed for changes. 11 12Create signals using the `signal()` function, which returns an object with three methods: 13 14- `get()` returns the current value 15- `set(newValue)` updates the value and notifies subscribers 16- `subscribe(callback)` registers a listener for changes 17 18Signals use strict equality (`===`) to determine if a value has changed. 19Setting a signal to its current value will not trigger notifications. 20 21### Computed Values 22 23Computed signals derive their values from other signals. They automatically track dependencies and recalculate only when those dependencies change. 24 25The `computed()` function takes a calculation function and a dependency array. 26The framework ensures computed values stay synchronized with their sources. 27 28Computed values are read-only and should not produce side effects. They exist purely to transform or combine other state. 29 30### Effects 31 32Effects run side effects in response to signal changes. The `effect()` function executes immediately and re-runs whenever its dependencies update. 33 34Common uses include: 35 36- Synchronizing with external APIs 37- Logging or analytics 38- Coordinating multiple signals 39 40For asynchronous operations, use `asyncEffect()` (see [asyncEffect](./async-effect)) which handles cleanup of pending operations when dependencies change or the effect is disposed. 41 42## Declarative State 43 44The preferred approach for most applications is declaring state directly in HTML using the `data-volt-state` attribute. This eliminates the need to write JavaScript for basic state management. 45 46State is declared as inline JSON on any element with the `data-volt` attribute: 47 48```html 49<div data-volt data-volt-state='{"count": 0, "items": []}'> 50``` 51 52The framework automatically converts these values into reactive signals. 53Nested objects and arrays become reactive, and property access in expressions automatically unwraps signal values. 54 55### Computed Values in Markup 56 57Derive values declaratively using `data-volt-computed:name` attributes. 58The name becomes a signal in the scope, and the attribute value is the computation expression: 59 60```html 61<div data-volt 62 data-volt-state='{"count": 5}' 63 data-volt-computed:doubled="count * 2"> 64``` 65 66Computed values defined this way follow the same rules as programmatic computed signals: they track dependencies and update automatically. 67For multi-word signal names, prefer kebab-case in the attribute (e.g., `data-volt-computed:active-todos`) — HTML lowercases attribute names and Volt converts kebab-case back to camelCase (`activeTodos`) automatically. 68 69## Programmatic State 70 71For complex applications requiring initialization logic or external API integration, create signals programmatically and pass them to the `mount()` function. 72 73This approach gives you full control over signal creation, composition, and lifecycle. Use it when: 74 75- State initialization requires async operations 76- Signals need to be shared across multiple mount points 77- Complex validation or transformation logic is needed 78- Integration with external state management is required 79 80## Scope and Access 81 82Each mounted element creates a scope containing its signals and computed values. 83Bindings access signals by property path relative to their scope. 84 85When using declarative state, the scope is built automatically from `data-volt-state` and `data-volt-computed:*` attributes. 86 87When using programmatic mounting, the scope is the object passed as the second argument to `mount()`. 88 89Bindings can access nested properties, and the evaluator automatically unwraps signal values. 90Event handlers receive special scope additions: `$el` for the element and `$event` for the event object. 91 92## Signal Auto-Unwrapping 93 94VoltX automatically unwraps signals in read contexts, making expressions simpler and more natural: 95 96```html 97<div data-volt data-volt-state='{"count": 5, "name": "Alice"}'> 98 <!-- Signals are automatically unwrapped in bindings --> 99 <p data-volt-text="count"></p> 100 <p data-volt-if="count > 0">Count is positive</p> 101 <p data-volt-if="name === 'Alice'">Hello Alice!</p> 102 103 <!-- In event handlers, use .get() to read and .set() to write --> 104 <button data-volt-on-click="count.set(count.get() + 1)">Increment</button> 105</div> 106``` 107 108**Read Contexts** (signals auto-unwrapped): 109 110- `data-volt-text`, `data-volt-html` 111- `data-volt-if`, `data-volt-else` 112- `data-volt-for` 113- `data-volt-class`, `data-volt-style` 114- `data-volt-bind:*` 115- `data-volt-computed:*` expressions 116 117**Write Contexts** (signals not auto-unwrapped): 118 119- `data-volt-on-*` event handlers 120- `data-volt-init` initialization code 121- `data-volt-model` (handles both read and write automatically) 122 123This design allows strict equality comparisons (`===`) to work naturally in conditional rendering while preserving access to signal methods like `.set()` in event handlers. 124 125## State Persistence 126 127Signals can be synchronized with browser storage using the built-in persist plugin. 128See the plugin documentation (coming soon!) for details on localStorage, sessionStorage, and IndexedDB integration. 129 130## State Serialization 131 132For server-side rendering, signals can be serialized to JSON and embedded in HTML for hydration on the client. This preserves state across the server-client boundary. 133 134Only serialize base signals containing primitive values, arrays, and plain objects. Computed signals are recalculated during hydration and should not be serialized. 135 136See the [Server-Side Rendering guide](./ssr) for complete hydration patterns. 137 138## Guidelines 139 140### Performance 141 142- Keep signal values immutable when possible. Create new objects rather than mutating existing ones 143- Use computed signals to avoid redundant calculations 144- Avoid creating signals inside loops or frequently-called functions 145 146### Architecture 147 148- Prefer declarative state for simple, self-contained components 149- Use programmatic state for complex initialization or cross-component coordination 150- Keep state close to where it's used: avoid deeply nested property access 151- Structure state with consistent shapes to prevent runtime errors in expressions 152 153### Debugging 154 155Signal updates are synchronous and deterministic. To trace state changes: 156 157- Use browser DevTools to set breakpoints in signal `.set()` calls 158- Subscribe to signals and log changes for debugging 159- Enable VoltX.js lifecycle hooks to observe mount and binding creation 160 161All errors in effects and subscriptions are caught and logged rather than thrown, preventing cascade failures.