CxJS

Store

import { Store } from 'cx/data'; Copied

The Store is the central state container in CxJS. It holds all application data and notifies widgets when data changes, triggering automatic UI updates.

Accessing the Store

Every CxJS widget has access to the store through its instance. In event handlers, access it via the second parameter:

<div class="flex flex-col gap-4">
  <div class="flex gap-2 items-center">
    <Button
      onClick={(e, { store }) => {
        store.set(m.count, (store.get(m.count) || 0) - 1)
      }}
    >
      -
    </Button>
    <span class="w-12 text-center" text={m.count} />
    <Button
      onClick={(e, { store }) => {
        store.set(m.count, (store.get(m.count) || 0) + 1)
      }}
    >
      +
    </Button>
  </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
{}

The store is available in event handlers like onClick, onChange, and others. Use store.get() to read values and store.set() to write them.

When using typed models with createModel, all store methods are fully typed. TypeScript catches type mismatches at compile time:

interface PageModel {
  count: number;
  name: string;
}

const m = createModel<PageModel>();

store.set(m.count, 5); // ✓ OK
store.set(m.count, "five"); // ✗ Type error
store.set(m.name, "John"); // ✓ OK

Store Methods

The store provides several methods for working with data:

MethodDescription
get(accessor)Returns the value at the given path
set(accessor, value)Sets the value at the given path
init(accessor, value)Sets the value only if currently undefined
update(accessor, fn)Applies a function to the current value and stores the result
delete(accessor)Removes the value at the given path
toggle(accessor)Inverts a boolean value
copy(from, to)Copies a value from one path to another
move(from, to)Moves a value from one path to another
batch(fn)Batches multiple updates, notifying listeners only once at the end
silently(fn)Executes updates without triggering any notifications
notify(path?)Manually triggers change notifications
subscribe(fn)Registers a listener for changes; returns an unsubscribe function
ref(accessor, defaultValue?)Creates a reactive reference to store data
getData()Returns the entire store data object
<div class="flex flex-col gap-4">
  <div class="flex flex-wrap gap-2">
    <Button
      onClick={(e, { store }) => {
        store.set(m.user.name, "John")
      }}
    >
      set(m.user.name, "John")
    </Button>
    <Button
      onClick={(e, { store }) => {
        store.update(m.user.age, (age) => (age || 0) + 1)
      }}
    >
      update(m.user.age, age =&gt; age + 1)
    </Button>
    <Button
      onClick={(e, { store }) => {
        store.delete(m.user.name)
      }}
    >
      delete(m.user.name)
    </Button>
    <Button
      onClick={(e, { store }) => {
        store.set(m.user, { name: "Jane", age: 25 })
      }}
    >
      set(m.user, &#123;...&#125;)
    </Button>
  </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
{}

Immutability

The store treats all data as immutable. When you call set() or update(), CxJS creates new object references along the path to the changed value. This enables efficient change detection — widgets only re-render when their bound data actually changes.

Warning

When updating objects or arrays, you must create new instances. Mutating existing objects directly will not trigger UI updates.

// Updating an object - spread the original and override properties
store.update(m.user, (user) => ({ ...user, name: "John" }));

// Adding to an array - create a new array
store.update(m.items, (items) => [...items, newItem]);

// Removing from an array - filter returns a new array
store.update(m.items, (items) => items.filter((item) => item.id !== id));

Immer Integration

For complex nested updates, manually spreading objects can become tedious. The cx-immer package provides a mutate method that lets you write mutable-style code while CxJS handles immutability behind the scenes:

import { enableImmerMutate } from "cx-immer";
enableImmerMutate();

// Now you can use mutate with mutable syntax
store.mutate(m.user, (user) => {
  user.name = "John";
  user.scores.push(100);
  user.address.city = "New York";
});

The mutate method uses Immer to produce immutable updates from mutable code. This is especially useful when updating deeply nested structures or performing multiple changes at once.

Creating a Store

In most applications, CxJS creates the store automatically. If you need to create one manually (for testing or advanced scenarios), use the Store constructor:

import { Store } from "cx/data";

const store = new Store({
  data: {
    count: 0,
    user: { name: "Guest" },
  },
});

// Read and write data
const count = store.get(m.count);
store.set(m.count, count + 1);