DEV Community

Yuuichi Eguchi
Yuuichi Eguchi

Posted on

Constela: New Features for Building Efficient SNS-like UIs

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 }
}
Enter fullscreen mode Exit fullscreen mode

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 }
}
Enter fullscreen mode Exit fullscreen mode

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": [...]
  }
}
Enter fullscreen mode Exit fullscreen mode

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" } }
          ]
        }
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

How it works:

  1. Button click triggers the toggleLike action
  2. setPath updates only posts[i].liked immutably
  3. Key-based diffing re-renders only that single card
  4. 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" }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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" }
        }
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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": "" }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Closing Connections

{
  "name": "disconnect",
  "steps": [
    { "do": "close", "connection": "chat" }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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" } }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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" }]} />
Enter fullscreen mode Exit fullscreen mode

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")} />
Enter fullscreen mode Exit fullscreen mode

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.


Links

Top comments (0)