Global State
VoltX provides built-in global state management through special variables and a globally available store. These features enable sharing state across components, accessing metadata, and coordinating behavior without external dependencies.
Overview
Every Volt scope automatically receives special variables (prefixed with $) that provide access to:
- Global Store - Shared reactive state across all scopes
- Scope Metadata - Information about the current reactive context
- Element References - Access to pinned DOM elements
- Utility Functions - Helper functions for common tasks
Special Variables
$store
Access globally shared reactive state across all Volt roots.
Declarative API:
<!-- Define global store -->
<script type="application/json" data-volt-store>
{
"theme": "dark",
"user": { "name": "Alice" }
}
</script>
<!-- Use in any Volt root -->
<div data-volt>
<p data-volt-text="$store.get('theme')"></p>
<button data-volt-on-click="$store.set('theme', 'light')">Toggle</button>
</div>Programmatic API:
import { registerStore, getStore } from 'voltx.js';
// Register store with signals or raw values
registerStore({
theme: signal('dark'),
count: 0 // Auto-wrapped in signal
});
// Access store
const store = getStore();
store.set('count', 5);
console.log(store.get('count')); // 5Methods:
$store.get(key)- Get signal value$store.set(key, value)- Update signal value$store.has(key)- Check if key exists$store[key]- Direct signal access (auto-unwrapped in read contexts)
Note on Signal Unwrapping:
When accessing store values via $store[key] in read contexts (like data-volt-text or data-volt-if), the signal is automatically unwrapped. In event handlers, use .get() and .set() methods for explicit control:
<!-- Read context: signal auto-unwrapped -->
<p data-volt-if="$store.theme === 'dark'">Dark mode active</p>
<!-- Event handler: use methods -->
<button data-volt-on-click="$store.theme.set('light')">Switch to Light</button>
<!-- Or use the store's convenience methods -->
<button data-volt-on-click="$store.set('theme', 'light')">Switch to Light</button>$origin
Reference to the root element of the current reactive scope.
<div data-volt id="app-root">
<p data-volt-text="'Root ID: ' + $origin.id"></p>
<!-- Displays: "Root ID: app-root" -->
</div>$scope
Direct access to the raw scope object containing all signals and context.
<div data-volt data-volt-state='{"count": 0}'>
<p data-volt-text="Object.keys($scope).length"></p>
<!-- Shows number of scope properties -->
</div>$pins
Access DOM elements registered with data-volt-pin.
<div data-volt>
<input data-volt-pin="username" />
<input data-volt-pin="password" type="password" />
<button data-volt-on-click="$pins.username.focus()">
Focus Username
</button>
<button data-volt-on-click="$pins.password.value = ''">
Clear Password
</button>
</div>Notes:
- Pins are scoped to their root element
- Each root maintains its own pin registry
- Pins are accessible immediately after registration
$pulse(callback)
Defers callback execution to the next microtask, ensuring DOM updates have completed.
<div data-volt data-volt-state='{"count": 0}'>
<button data-volt-on-click="count.set(count.get() + 1); $pulse(() => console.log('Updated!'))">
Increment
</button>
</div>Use Cases:
- Run code after DOM updates
- Coordinate async operations
- Batch multiple updates
$uid(prefix?)
Generates unique, deterministic IDs within the scope.
<div data-volt>
<input data-volt-bind:id="$uid('field')" />
<!-- id="volt-field-1" -->
<input data-volt-bind:id="$uid('field')" />
<!-- id="volt-field-2" -->
<input data-volt-bind:id="$uid()" />
<!-- id="volt-3" -->
</div>Notes:
- IDs are unique within the scope
- Counter increments on each call
- Different scopes have independent counters
$arc(eventName, detail?)
Dispatches a CustomEvent from the current element.
<div data-volt data-volt-on-user:save="console.log('Saved:', $event.detail)">
<button data-volt-on-click="$arc('user:save', { id: 123, name: 'Alice' })">
Save User
</button>
</div>Event Properties:
bubbles: true- Event bubbles up the DOMcomposed: true- Crosses shadow DOM boundariescancelable: true- Can be preventeddetail- Custom data payload
$probe(expression, callback)
Observes a reactive expression and calls a callback when dependencies change.
<div data-volt data-volt-state='{"count": 0}' data-volt-init="$probe('count', v => console.log('Count:', v))">
<button data-volt-on-click="count.set(count.get() + 1)">Increment</button>
<!-- Logs: "Count: 0" immediately, then "Count: 1", "Count: 2", etc. -->
</div>Parameters:
expression(string) - Reactive expression to observecallback(function) - Called with expression value on changes
Returns:
- Cleanup function to stop observing
Example:
<div data-volt
data-volt-state='{"x": 0, "y": 0}'
data-volt-init="const cleanup = $probe('x + y', sum => console.log('Sum:', sum))">
<button data-volt-on-click="x.set(x.get() + 1)">+X</button>
<button data-volt-on-click="y.set(y.get() + 1)">+Y</button>
<!-- Logs: "Sum: 0" initially, then on every change -->
</div>data-volt-init
Run initialization code once when an element is mounted.
Basic Usage:
<div data-volt
data-volt-state='{"initialized": false}'
data-volt-init="initialized.set(true)">
<p data-volt-text="initialized"></p>
<!-- Displays: true -->
</div>Setting Up Observers:
<div data-volt
data-volt-state='{"count": 0, "log": []}'
data-volt-init="$probe('count', v => log.push(v))">
<button data-volt-on-click="count.set(count.get() + 1)">Increment</button>
<p data-volt-text="log.join(', ')"></p>
<!-- Displays: "0, 1, 2, ..." -->
</div>Accessing Special Variables:
<div data-volt
id="main"
data-volt-state='{"rootId": ""}'
data-volt-init="rootId.set($origin.id)">
<p data-volt-text="rootId"></p>
<!-- Displays: "main" -->
</div>Global Store Patterns
Shared Application State
<!-- Define global state once -->
<script type="application/json" data-volt-store>
{
"theme": "light",
"user": null,
"authenticated": false
}
</script>
<!-- Header component -->
<div data-volt>
<div data-volt-class="$store.get('theme')">
<button data-volt-on-click="$store.set('theme', $store.get('theme') === 'light' ? 'dark' : 'light')">
Toggle Theme
</button>
</div>
</div>
<!-- User profile -->
<div data-volt>
<div data-volt-if="$store.get('authenticated')">
<p data-volt-text="'Welcome, ' + $store.get('user').name"></p>
</div>
</div>Cross-Component Communication
<script type="application/json" data-volt-store>
{
"selectedId": null,
"items": []
}
</script>
<!-- Item list -->
<div data-volt>
<div data-volt-for="item in $store.get('items')">
<button data-volt-on-click="$store.set('selectedId', item.id)" data-volt-text="item.name"></button>
</div>
</div>
<!-- Item details -->
<div data-volt>
<div data-volt-if="$store.get('selectedId')">
<p data-volt-text="'Selected: ' + $store.get('selectedId')"></p>
</div>
</div>Persistent Global State
import { registerStore, getStore } from 'voltx.js';
import { registerPlugin, persistPlugin } from 'voltx.js';
// Register persist plugin
registerPlugin('persist', persistPlugin);
// Initialize store with persisted values
const saved = localStorage.getItem('app-store');
const initialState = saved ? JSON.parse(saved) : { theme: 'light', user: null };
registerStore(initialState);
// Save on changes
const store = getStore();
const originalSet = store.set.bind(store);
store.set = (key, value) => {
originalSet(key, value);
localStorage.setItem('app-store', JSON.stringify({
theme: store.get('theme'),
user: store.get('user')
}));
};Best Practices
Use $store for Shared State
Global state should live in $store:
<!-- Good: Shared theme in store -->
<script type="application/json" data-volt-store>
{ "theme": "dark" }
</script>
<div data-volt>
<p data-volt-class="$store.get('theme')">Content</p>
</div>Use $pins for Element Access
Access DOM elements through pins instead of querySelector:
<!-- Good: Using pins -->
<div data-volt>
<input data-volt-pin="username" />
<button data-volt-on-click="$pins.username.focus()">Focus</button>
</div>
<!-- Avoid: Manual querySelector -->
<div data-volt>
<input id="username" />
<button data-volt-on-click="document.querySelector('#username').focus()">Focus</button>
</div>Use data-volt-init for Setup
Initialize observers and one-time setup in data-volt-init:
<div data-volt
data-volt-state='{"count": 0}'
data-volt-init="$probe('count', v => console.log('Count:', v))">
<!-- Component content -->
</div>Scope Pin Names Appropriately
Use descriptive pin names and avoid collisions:
<!-- Good: Descriptive names -->
<div data-volt>
<input data-volt-pin="searchInput" />
<input data-volt-pin="filterInput" />
</div>
<!-- Avoid: Generic names that might collide -->
<div data-volt>
<input data-volt-pin="input" />
<input data-volt-pin="input2" />
</div>Clean Up Observers
Always clean up $probe observers when no longer needed:
<div data-volt
data-volt-state='{"active": true}'
data-volt-init="const cleanup = $probe('active', v => console.log(v))">
<button data-volt-on-click="active.set(false); cleanup()">Deactivate</button>
</div>Examples
Todo App with Global State
<script type="application/json" data-volt-store>
{
"todos": [],
"filter": "all"
}
</script>
<!-- Add todo form -->
<div data-volt data-volt-state='{"newTodo": ""}'>
<input data-volt-model="newTodo" data-volt-pin="todoInput" />
<button data-volt-on-click="$store.set('todos', [...$store.get('todos'), { text: newTodo.get(), done: false }]); newTodo.set(''); $pins.todoInput.focus()">
Add
</button>
</div>
<!-- Filter buttons -->
<div data-volt>
<button data-volt-on-click="$store.set('filter', 'all')">All</button>
<button data-volt-on-click="$store.set('filter', 'active')">Active</button>
<button data-volt-on-click="$store.set('filter', 'done')">Done</button>
</div>
<!-- Todo list -->
<div data-volt>
<div data-volt-for="todo in $store.get('todos')">
<div data-volt-if="$store.get('filter') === 'all' || ($store.get('filter') === 'done' && todo.done) || ($store.get('filter') === 'active' && !todo.done)">
<input type="checkbox" data-volt-bind:checked="todo.done" />
<span data-volt-text="todo.text"></span>
</div>
</div>
</div>Multi-Step Form
<script type="application/json" data-volt-store>
{
"step": 1,
"formData": { "name": "", "email": "", "phone": "" }
}
</script>
<div data-volt>
<!-- Step indicator -->
<p data-volt-text="'Step ' + $store.get('step') + ' of 3'"></p>
<!-- Step 1: Name -->
<div data-volt-if="$store.get('step') === 1">
<input data-volt-model="$store.get('formData').name" placeholder="Name" />
<button data-volt-on-click="$store.set('step', 2)">Next</button>
</div>
<!-- Step 2: Email -->
<div data-volt-if="$store.get('step') === 2">
<input data-volt-model="$store.get('formData').email" placeholder="Email" />
<button data-volt-on-click="$store.set('step', 1)">Back</button>
<button data-volt-on-click="$store.set('step', 3)">Next</button>
</div>
<!-- Step 3: Phone -->
<div data-volt-if="$store.get('step') === 3">
<input data-volt-model="$store.get('formData').phone" placeholder="Phone" />
<button data-volt-on-click="$store.set('step', 2)">Back</button>
<button data-volt-on-click="console.log('Submit:', $store.get('formData'))">Submit</button>
</div>
</div>API Reference
registerStore(state)
Register global store state programmatically.
import { registerStore } from 'voltx.js';
import { signal } from 'voltx.js';
registerStore({
theme: signal('dark'), // Existing signal
count: 0 // Auto-wrapped
});getStore()
Get the global store instance.
import { getStore } from 'voltx.js';
const store = getStore();
store.set('theme', 'light');
console.log(store.get('theme')); // 'light'
console.log(store.has('theme')); // truegetScopeMetadata(scope)
Get metadata for a scope (advanced use).
import { getScopeMetadata } from 'voltx.js';
const metadata = getScopeMetadata(scope);
console.log(metadata.origin); // Root element
console.log(metadata.pins); // Pin registry
console.log(metadata.uidCounter); // Current UID counter