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.