Introduction
Constela is a framework that lets you build UIs using only JSON DSL—no JavaScript or TypeScript required. This update introduces features specifically designed to handle screens with many interactive elements, like social media feeds with hundreds of "like" buttons.
The problems we solved:
- Changing 1 like in a list of 1000 posts → all 1000 items re-render
- No way to update only a nested value like
posts[5].liked - All list items re-render when the array changes
Here are 2 new features that address these challenges.
1. setPath - Partial Updates for Nested State
The Problem
Previously, the update action required replacing entire arrays. Updating all 1000 posts just to change one like is inefficient.
The Solution
The setPath action lets you update a specific field of a specific item in an array:
{
"do": "setPath",
"target": "posts",
"path": [5, "liked"],
"value": { "expr": "lit", "value": true }
}
This updates only posts[5].liked immutably.
Dynamic Path
You can specify the index dynamically:
{
"do": "setPath",
"target": "posts",
"path": { "expr": "var", "name": "payload", "path": "index" },
"field": "liked",
"value": { "expr": "lit", "value": true }
}
2. Key-based List Diffing
The Problem
Previously, each loops re-rendered all items whenever the array changed.
The Solution
By specifying a key, items with the same key reuse their DOM nodes:
{
"kind": "each",
"items": { "expr": "state", "name": "posts" },
"as": "post",
"key": { "expr": "var", "name": "post", "path": "id" },
"body": {
"kind": "element",
"tag": "article",
"props": { "className": "post-card" },
"children": [...]
}
}
Behavior Comparison
| Operation | Without key | With key |
|---|---|---|
| Add 1 item | Re-render all | Add 1 DOM node |
| Remove 1 item | Re-render all | Remove 1 DOM node |
| Reorder | Re-render all | Move DOM nodes only |
| Update 1 item | Re-render all | Update only that item |
Input State Preservation
With key-based diffing, user input text and focus state are preserved during updates. Form inputs won't be lost when the list updates.
Practical Example: SNS "Like" Button
Here's a complete example combining these features. This UI handles 1000 posts efficiently:
{
"state": {
"posts": { "type": "list", "initial": [] }
},
"actions": [
{
"name": "toggleLike",
"steps": [
{
"do": "setPath",
"target": "posts",
"path": { "expr": "var", "name": "payload", "path": "index" },
"field": "liked",
"value": {
"expr": "not",
"operand": { "expr": "var", "name": "payload", "path": "currentLiked" }
}
}
]
}
],
"view": {
"kind": "each",
"items": { "expr": "state", "name": "posts" },
"as": "post",
"index": "i",
"key": { "expr": "var", "name": "post", "path": "id" },
"body": {
"kind": "element",
"tag": "article",
"props": { "className": "post-card" },
"children": [
{
"kind": "element",
"tag": "p",
"children": [
{ "kind": "text", "value": { "expr": "var", "name": "post", "path": "content" } }
]
},
{
"kind": "element",
"tag": "button",
"props": {
"className": {
"expr": "cond",
"test": { "expr": "var", "name": "post", "path": "liked" },
"then": { "expr": "lit", "value": "like-btn active" },
"else": { "expr": "lit", "value": "like-btn" }
},
"onClick": {
"action": "toggleLike",
"payload": {
"index": { "expr": "var", "name": "i" },
"currentLiked": { "expr": "var", "name": "post", "path": "liked" }
}
}
},
"children": [
{ "kind": "text", "value": { "expr": "lit", "value": "Like" } }
]
}
]
}
}
}
How it works:
- Button click triggers the
toggleLikeaction -
setPathupdates onlyposts[i].likedimmutably - Key-based diffing re-renders only that single card
- The other 999 posts remain untouched
Other Features in This Release
In addition to the performance improvements above, the following features were also added in this release.
WebSocket Support
Define WebSocket connections declaratively for real-time communication:
{
"connections": {
"chat": {
"type": "websocket",
"url": "wss://api.example.com/ws",
"onMessage": { "action": "handleMessage" },
"onOpen": { "action": "connectionOpened" },
"onClose": { "action": "connectionClosed" }
}
}
}
Receiving Messages
Received messages are passed to the action as the payload variable:
{
"actions": [
{
"name": "handleMessage",
"steps": [
{
"do": "update",
"target": "messages",
"operation": "push",
"value": { "expr": "var", "name": "payload" }
}
]
}
]
}
Sending Messages
Use the send action to send messages through a connection:
{
"name": "sendMessage",
"steps": [
{
"do": "send",
"connection": "chat",
"data": { "expr": "state", "name": "inputText" }
},
{
"do": "set",
"target": "inputText",
"value": { "expr": "lit", "value": "" }
}
]
}
Closing Connections
{
"name": "disconnect",
"steps": [
{ "do": "close", "connection": "chat" }
]
}
Real-time Chat Example
A complete chat application using WebSocket:
{
"state": {
"messages": { "type": "list", "initial": [] },
"inputText": { "type": "string", "initial": "" },
"connected": { "type": "boolean", "initial": false }
},
"connections": {
"chat": {
"type": "websocket",
"url": "wss://chat.example.com/ws",
"onMessage": { "action": "receiveMessage" },
"onOpen": { "action": "onConnected" },
"onClose": { "action": "onDisconnected" }
}
},
"actions": [
{
"name": "receiveMessage",
"steps": [
{
"do": "update",
"target": "messages",
"operation": "push",
"value": { "expr": "var", "name": "payload" }
}
]
},
{
"name": "sendMessage",
"steps": [
{
"do": "send",
"connection": "chat",
"data": {
"text": { "expr": "state", "name": "inputText" }
}
},
{
"do": "set",
"target": "inputText",
"value": { "expr": "lit", "value": "" }
}
]
},
{
"name": "onConnected",
"steps": [
{ "do": "set", "target": "connected", "value": { "expr": "lit", "value": true } }
]
},
{
"name": "onDisconnected",
"steps": [
{ "do": "set", "target": "connected", "value": { "expr": "lit", "value": false } }
]
}
],
"view": {
"kind": "element",
"tag": "div",
"props": { "className": "chat-container" },
"children": [
{
"kind": "each",
"items": { "expr": "state", "name": "messages" },
"as": "msg",
"key": { "expr": "var", "name": "msg", "path": "id" },
"body": {
"kind": "element",
"tag": "div",
"props": { "className": "message" },
"children": [
{ "kind": "text", "value": { "expr": "var", "name": "msg", "path": "text" } }
]
}
},
{
"kind": "element",
"tag": "input",
"props": {
"type": "text",
"value": { "expr": "state", "name": "inputText" },
"onInput": { "action": "updateInput" },
"placeholder": "Type a message..."
}
},
{
"kind": "element",
"tag": "button",
"props": {
"onClick": { "action": "sendMessage" },
"disabled": {
"expr": "not",
"operand": { "expr": "state", "name": "connected" }
}
},
"children": [
{ "kind": "text", "value": { "expr": "lit", "value": "Send" } }
]
}
]
}
}
For TypeScript Developers: Type-safe State Access
When extending Constela runtime with TypeScript, you can use TypedStateStore for type-safe state access:
import { createTypedStateStore } from '@constela/runtime';
interface AppState {
posts: { id: number; liked: boolean }[];
filter: string;
}
const state = createTypedStateStore<AppState>({
posts: { type: 'list', initial: [] },
filter: { type: 'string', initial: '' },
});
state.get('posts'); // Type: { id: number; liked: boolean }[]
state.get('filter'); // Type: string
state.set('filter', 'recent'); // OK
state.set('filter', 123); // TypeScript error
This is an advanced feature for developers who work directly with the runtime instead of using JSON DSL.
MDX Parser Security Improvements
Security checking for attribute expressions in MDX files has been improved.
Previously, words like "require" inside string literals would incorrectly match security patterns and fail silently:
<!-- Before: Silent failure → empty table -->
<PropsTable items={[{ description: "operations that require one" }]} />
String literal contents are now excluded from security checks—only actual code is validated:
<!-- OK: "require" is inside a string literal -->
<PropsTable items={[{ description: "operations that require one" }]} />
<!-- Error: require() is actual code -->
<Button data={require("module")} />
When dangerous patterns are detected, explicit error messages are now displayed.
Summary
Performance Improvements:
| Feature | Problem Solved |
|---|---|
| setPath | Partial updates for nested state |
| Key-based diffing | Unnecessary re-renders on list updates |
Other New Features:
| Feature | Description |
|---|---|
| WebSocket | Real-time communication |
| TypedStateStore | Type-safe state access (for TypeScript) |
| MDX Security | Allow safe words in string literals, compile-time errors for dangerous code |
All features work with JSON DSL (except TypedStateStore).
These features are opt-in and don't affect existing code.
Top comments (0)