JSON Path Reactive eXpressions - A reactive expression language for JSON data
npm install jprx=(expr) for unambiguous expression parsing. Legacy prefix-only syntax (e.g., =/path) is still supported but will be deprecated after March 31, 2026.
eval(). Expressions are handled by a custom high-performance Pratt parser and a registry of pre-defined helpers, making it safe for dynamic content.
onmount) where state initialization can occur. JPRX relies on the library to trigger these initializers.
oninput, onclick) SHOULD be provided, though exact implementations may vary by platform.
=(/user/name) | Access global state via an absolute path. |
=(./count) | Access properties relative to the current context. |
=(../id) | Traverse up the state hierarchy (UP-tree search). |
=(sum(/items...price)) | Call registered core helpers. |
=(/items...name) | Extract a property from every object in an array (spread). |
=(++/count), =(/a + /b) | Familiar JS-style prefix, postfix, and infix operators. |
_ (item), $this, $event | Context-aware placeholders for iteration and interaction. |
=(bind(/user/name))| Create a managed, two-way reactive link for inputs. |
=(move(target, loc))| Decentralized layout: Move/replace host element into a target. |
state() and signal(), use =function(...) without the outer wrapper, as they execute once on mount rather than being reactive expressions.
scope property in the options argument.
$state or $signal.
=state(value, { name: 'user', schema: 'UserProfile', scope: event.target })
=signal(0, { name: 'count', schema: 'auto' })
jprx.registerSchema(name, definition).
javascript
// 1. Register a schema centrally
jprx.registerSchema('UserProfile', {
name: "string",
age: "number",
email: { type: "string", format: "email" }
});
// 2. Reference the registered schema by name (Scoped)
const user = =state({}, { name: 'user', schema: 'UserProfile', scope: $this });
// 3. Use the 'polymorphic' shorthand for auto-coercion
const settings = =state({ volume: 50 }, { name: 'settings', schema: 'polymorphic' });
// Result: settings.volume = "60" will automatically coerce to the number 60.
`
- Polymorphic Schemas:
- "auto": Infers the fixed schema from the initial value. Strict type checking (e.g., setting a number to a string throws). New properties are not allowed.
- "dynamic": Like auto, but allows new properties to be added to the state object.
- "polymorphic": Includes dynamic behavior and automatically coerces values to match the inferred type (e.g., "50" -> 50) rather than throwing.
- Shorthand: A simple object like { name: "string" } is internally normalized to a JSON Schema.
$3
Schemas can define transformations that occur during state updates, ensuring data remains in a consistent format regardless of how it was input.
`json
{
"type": "object",
"properties": {
"username": {
"type": "string",
"transform": "lower"
}
}
}
`
Note: The =bind helper uses these transformations to automatically clean data as the user types.
Two-Way Binding with
=bind
The =bind(path) helper creates a managed, two-way link between the UI and a state path.
$3
To ensure unambiguous data flow, =bind only accepts direct paths. It cannot be used directly with computed expressions like =bind(upper(/name)).
$3
If you need to transform data during a two-way binding, there are two primary approaches:
1. Event-Based: Use a manual oninput handler to apply the transformation, e.g., =(set(/name, upper($event/target/value))).
2. Schema-Based: Define a transform or pattern in the schema for the path. The =bind helper will respect the schema rules during the write-back phase.
---
DOM Patches & Decentralized Layouts
One of the most powerful features of JPRX in UI environments (like Lightview) is the ability to perform Decentralized DOM Patches via the =move(target, location) helper.
$3
When an LLM streams UI components, it often knows what it is creating before it knows exactly where that item belongs in a complex dashboard, or it may need to update an existing element that was created minutes ago.
$3
The =move helper allows a component to "place itself" into the document upon mounting.
`json
{
"tag": "div",
"id": "weather-widget",
"onmount": "=move('#dashboard-sidebar', 'afterbegin')",
"content": "Sunny, 75°F"
}
`
Key Behaviors:
1. Teleportation: The host element is physically moved from the "Stream Container" (where it was born) to the specified target (e.g., #dashboard-sidebar).
2. Idempotent Updates: If the moving element has an id (e.g., weather-widget) and an element with that same ID already exists at the destination, the existing element is replaced. This allows the LLM to "patch" the UI simply by streaming the updated version of the component.
3. Flicker-Free: By rendering the stream in a hidden container, the element is moved and appears in its final destination instantly.
$3
The =mount(url, options?) helper is the primary mechanism for fetching these decentralized updates.
1. Arrival: =mount fetches the content and "lands" it at the end of the document.body (by default).
2. Mounting: The content is hydrated and added to the DOM, triggering its onmount hook.
3. Teleportation: If the content contains =move, it immediately relocates itself to its destination.
This separation of concerns makes the system incredibly robust: =mount handles the delivery, while =move handles the logic of where the content belongs.
$3
- Low Overhead: The LLM doesn't need to maintain a map of the entire DOM; it just needs to know the ID or Selector of where it wants to "push" its content.
- Independence: Components are self-contained. A "Notification" component knows how to display itself AND where notification stacks live.
- Context Preservation: In Lightview, moved elements retain their original reactive context ("backpack"), allowing them to stay linked to the stream that created them.
---
Example
A modern, lifecycle-based reactive counter:
`json
{
"div": {
"onmount": "=state({ count: 0 }, { name: 'counter', schema: 'auto', scope: $this })",
"children": [
{ "h2": "Modern JPRX Counter" },
{ "p": ["Current Count: ", "=(/counter/count)"] },
{ "button": { "onclick": "=(++/counter/count)", "children": ["+"] } },
{ "button": { "onclick": "=(--/counter/count)", "children": ["-"] } }
]
}
}
``