CxJS

Typed Models

CxJS uses typed models to provide type-safe access to data in the store. Instead of using string paths like "user.firstName", you use accessor chains like m.user.firstName that are checked by TypeScript.

Creating a Model Proxy

Use createModel<T>() to create a proxy object that mirrors your data structure. The proxy doesn’t hold any data — it generates binding paths that connect widgets to the store.

<div class="flex flex-col gap-4">
  <div class="flex gap-2">
    <TextField value={m.user.firstName} placeholder="First name" />
    <TextField value={m.user.lastName} placeholder="Last name" />
  </div>
  <div class="p-3 bg-muted rounded text-sm">
    <strong>Store content</strong>
    <pre class="mt-2" text={(data) => JSON.stringify(data, null, 2)} />
  </div>
</div>
Store content
{}

When you write m.user.firstName, CxJS creates a binding to the path "user.firstName" in the store. The TextField reads and writes to this path automatically.

Why Typed Models?

Typed models provide several benefits over string-based paths:

  • Type safety — TypeScript catches typos and invalid paths at compile time
  • Autocomplete — Your editor suggests available properties as you type
  • Refactoring — Rename a property and all usages update automatically
  • Documentation — Hover over a property to see its type

Accessor Methods

Accessor chains provide two useful methods for working with paths:

MethodDescription
toString()Returns the full string path represented by the accessor. Useful when you need to pass paths to APIs that expect strings.
nameOf()Returns only the last segment of the path (the property name).
<table class="w-full text-sm">
  <thead>
    <tr>
      <th class="text-left py-2 pr-4 border-b border-border">Accessor</th>
      <th class="text-left py-2 border-b border-border">Result</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td class="py-2 pr-4">
        <code class="text-primary">m.user.firstName.toString()</code>
      </td>
      <td class="py-2">
        <code>"{m.user.firstName.toString()}"</code>
      </td>
    </tr>
    <tr>
      <td class="py-2 pr-4">
        <code class="text-primary">m.user.email.toString()</code>
      </td>
      <td class="py-2">
        <code>"{m.user.email.toString()}"</code>
      </td>
    </tr>
    <tr>
      <td class="py-2 pr-4">
        <code class="text-primary">m.count.toString()</code>
      </td>
      <td class="py-2">
        <code>"{m.count.toString()}"</code>
      </td>
    </tr>
    <tr>
      <td class="py-2 pr-4">
        <code class="text-primary">m.user.lastName.nameOf()</code>
      </td>
      <td class="py-2">
        <code>"{m.user.lastName.nameOf()}"</code>
      </td>
    </tr>
    <tr>
      <td class="py-2 pr-4">
        <code class="text-primary">m.count.nameOf()</code>
      </td>
      <td class="py-2">
        <code>"{m.count.nameOf()}"</code>
      </td>
    </tr>
  </tbody>
</table>
AccessorResult
m.user.firstName.toString()"user.firstName"
m.user.email.toString()"user.email"
m.count.toString()"count"
m.user.lastName.nameOf()"lastName"
m.count.nameOf()"count"

Nested Structures

Accessor chains work with deeply nested structures. Define your interfaces to match your data shape:

interface Address {
  street: string;
  city: string;
  country: string;
}

interface User {
  name: string;
  address: Address;
}

interface PageModel {
  user: User;
}

const m = createModel<PageModel>();

// Access nested properties
m.user.address.city; // binds to "user.address.city"

The proxy automatically generates the correct path regardless of nesting depth.

Note

createModel can also be imported from cx/ui. createAccessorModelProxy is available as an alias for backward compatibility.