State (Store)
State provides deep reactivity for objects and arrays. Unlike signals which only track reassignment, state tracks nested property changes automatically.
The Shortcomings of Signals
// Signals only react to reassignment
const user = signal({ name: 'Alice', age: 25 });
user.value.age = 26; // ❌ Won't trigger updates!
user.value = { ...user.value, age: 26 }; // ✅ Works, but verbose
// Arrays have the same issue
const items = signal([1, 2, 3]);
items.value.push(4); // ❌ Won't trigger updates!
items.value = [...items.value, 4]; // ✅ Works, but tedious
State to the Rescue
const { state } = LightviewX;
// Deep reactivity - mutations work!
const user = state({ name: 'Alice', age: 25 });
user.age = 26; // ✅ Triggers updates!
user.name = 'Bob'; // ✅ Triggers updates!
// Arrays just work
const items = state([1, 2, 3]);
items.push(4); // ✅ Triggers updates!
items[0] = 10; // ✅ Triggers updates!
items.sort(); // ✅ Triggers updates!
State Function
The state function is the primary initializer for reactive stores.
state(initialValue, nameOrOptions?)
Parameters
initialValue: The object or array to be wrapped in a reactive proxy.nameOrOptions(Optional):- If a string: This becomes the
nameof the state for global registration. - If an object: Supported keys include:
name: A unique identifier for the state in the registry.storage: An object implementing the Storage interface (e.g.,localStorage) to enable persistence.scope: A DOM element or object to bind the state's visibility for up-tree lookups.schema: A validation behavior ("auto","dynamic","polymorphic") or a formal registered JSON Schema name (see JSON Schema Lite below).
- If a string: This becomes the
See the examples in the sections below for detailed usage of these parameters.
Nested Objects
State tracks changes at any depth:
const app = state({
user: {
profile: {
name: 'Alice',
settings: {
theme: 'dark',
notifications: true
}
}
},
items: []
});
// All of these trigger updates:
app.user.profile.name = 'Bob';
app.user.profile.settings.theme = 'light';
app.items.push({ id: 1, text: 'Hello' });
In the UI
const { state } = LightviewX;
const { tags, $ } = Lightview;
const { div, ul, li, input, span, button } = tags;
const todos = state([
{ text: 'Learn Lightview', done: true },
{ text: 'Build app', done: false }
]);
const app = div({ style: 'padding: 1rem;' },
ul({ style: 'list-style: none; padding: 0; margin-bottom: 1rem;' },
() => todos.map((todo, i) =>
li({ style: 'display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;' },
input({
type: 'checkbox',
checked: todo.done,
onchange: () => todos[i].done = !todos[i].done,
style: 'cursor: pointer;'
}),
span({
style: () => todo.done ? 'text-decoration: line-through; opacity: 0.6;' : ''
}, todo.text)
)
)
),
button({
onclick: () => todos.push({ text: `Task ${todos.length + 1}`, done: false }),
}, '+ Add Task')
);
$('#example').content(app);
Array Methods
All mutating array methods are reactive:
const items = state([1, 2, 3]);
items.push(4); // Add to end
items.pop(); // Remove from end
items.shift(); // Remove from start
items.unshift(0); // Add to start
items.splice(1, 1); // Remove at index
items.sort(); // Sort in place
items.reverse(); // Reverse in place
items.fill(0); // Fill with value
Date Methods
When a property is a Date object, all mutating methods are reactive. This includes all
set* methods (e.g. setFullYear(), setDate(), setHours(),
etc.):
const event = state({ date: new Date() });
// This will trigger UI updates
event.date.setFullYear(2025);
Named State
Like signals, you can name state objects for global access. This is especially useful for shared application state:
// Create named state
const appState = state({
user: 'Guest',
theme: 'dark'
}, 'app');
// Retrieve it anywhere
const globalState = state.get('app');
// Get or create
const settings = state.get('settings', { notifications: true });
Stored State
You can store named state objects in Storage objects (e.g. sessionStorage or localStorage) for persistence. It will be saved any time there is a change. Objects are automatically serialized to JSON and deserialized back to objects.
const user = state({name:'Guest', theme:'dark'}, {name:'user', storage:sessionStorage});
// Retrieve it elsewhere (even in another file)
const sameUser = state.get('user');
// Get or create with default value
// If 'user' exists, returns it. If not, creates it with default value.
const score = state.get('user', {storage:sessionStorage, defaultValue:{name:'Guest', theme:'dark'}});
Note: Manually updating the object in storage will not trigger updates.
Schema Validation
State objects can be validated and transformed using the schema option. This ensures data
integrity and can automatically coerce values to the expected types.
const user = state({ name: 'Alice', age: 25 }, { schema: 'auto' });
user.age = 26; // ✅ Works (same type)
user.age = '30'; // ❌ Throws: Type mismatch
user.status = 'active'; // ❌ Throws: Cannot add new property
Built-in Schema Behaviors
| Behavior | Description |
|---|---|
"auto" |
Infers a fixed schema from the initial value. Prevents adding new properties and enforces strict type checking. |
"dynamic" |
Like auto, but allows the state object to grow with new properties. |
"polymorphic" |
Allows growth and automatically coerces values to match the initial type (e.g., setting "100" to a number property saves it as the number 100). |
// Polymorphic coerces types automatically
const settings = state({ volume: 50, muted: false }, { schema: 'polymorphic' });
settings.volume = '75'; // ✅ Coerced to number 75
settings.muted = 'true'; // ✅ Coerced to boolean true
settings.newProp = 'ok'; // ✅ Allowed (dynamic growth)
JSON Schema Lite
When you load lightview-x.js, the schema engine is upgraded to a "JSON Schema Lite" validator.
It supports standard Draft 7 keywords while remaining incredibly lightweight.
Supported Keywords
| Type | Keywords |
|---|---|
| String | minLength, maxLength, pattern,
format: 'email'
|
| Number | minimum, maximum, multipleOf |
| Object | required, properties, additionalProperties: false |
| Array | items, minItems, maxItems, uniqueItems |
| General | type, enum, const |
Named Schemas
You can register schemas globally and reference them by name. This encourages reuse and keeps your
state initializations clean.
// 1. Register a schema
Lightview.registerSchema('User', {
type: "object",
properties: {
username: { type: "string", minLength: 3 },
email: { type: "string", format: "email" }
},
required: ["username"]
});
// 2. Use it by name
const user = state({ username: "alice" }, { schema: "User" });
Transformation Helpers
Unlike industry-standard validators (which only allow/disallow data), Lightview's engine supports declarative transformations. This allows you to specify how data should be "cleaned" before it hits the state.
const UserSchema = {
type: "object",
properties: {
username: {
type: "string",
transform: "lower" // Using a registered helper name
},
salary: {
type: "number",
transform: (v) => Math.round(v) // Using an inline function
}
}
};
const user = state({ username: 'ALICE', salary: 50212.55 }, { schema: UserSchema });
// user.username is 'alice' (via "lower")
// user.salary is 50213 (via inline function)
When using a string for the transform (like "lower"), Lightview looks for a
matching function in the global registry. This requires that the helper has been registered via JPRX
(requires lightview-cdom.js) or manually in Lightview.helpers. For more details on
built-in
helpers, see the cDOM documentation.
"lower", "round"). You do not need a
leading $.
Loading Helpers
When using the transform keyword, Lightview searches for the named function in the following
order:
- JPRX Helpers: Any helper registered via
registerHelper(requireslightview-cdom.js). - Public API: Functions attached to
Lightview.helpers. - Inline: You can also pass a function directly to the
transformproperty.
math, string, or
array helpers) for state transformations, you must ensure lightview-cdom.js is
loaded before your state is initialized.
Lite vs. Full Validators
Why use our "Lite" engine instead of a full validator like Ajv?
| Feature | Lightview Lite | Full (Ajv, etc.) |
|---|---|---|
| Size | ~2KB (built-in) | ~40KB+ (external) |
| Transformation | ✅ First-class support | ❌ Not supported (Validators only) |
| Performance | 🚀 Optimized for UI cycles | Heavy overhead for simple checks |
| Spec Compliance | Basic Draft 7 (No $ref) |
100% Full Specification |
Using a Full Validator
If you have complex enterprise schemas that require full specification compliance, you can swap out the Lightview engine for any industry-standard validator:
transform keyword in your schemas unless your external validator also supports it.
import Ajv from "ajv";
const ajv = new Ajv();
// Swap the validation hook
Lightview.internals.hooks.validate = (value, schema) => {
const valid = ajv.validate(schema, value);
if (!valid) throw new Error(ajv.errorsText());
return true;
};