DraftThis article is currently in draft mode

Building a World State

Adaptations
Original
react-jotai ×
TXT
October 15, 2025

Building a World State#

A world state is a reactive data structure that manages application state. In this article, we'll explore how to build a TodoMVC application using the view model pattern with LiveStore as our world state implementation.

Interactive Demo#

Todo List View

Architecture Overview#

This example uses React with LiveStore (an event-sourced reactive database) to manage application state. The view model pattern separates business logic from UI components.

Creating the View Model#

The view model (VM) defines the shape of our application's state and actions. Here's the TodoMVC VM structure:

import { computed, nanoid, queryDb } from "@livestore/livestore";
import type { Queryable, Store } from "@livestore/livestore";

import { DisposePool, dev, memoFn } from "@phosphor/utils";
import { emitDebugValue, emitDebugValueFn, emitFunctionCall } from "#scripts/lib/dev/emitDebugValue.ts";
import { uiState$ } from "./livestore/queries.js";
import { events, type schema, tables } from "./livestore/schema.js";

const pluralize = (count: number, singular: string, plural: string) => (count === 1 ? singular : plural);

export type TodoItemVM = {
  key: string;
  text$: Queryable<string>;
  completed$: Queryable<boolean>;
  toggleCompleted: () => void;-toggleCompleted
  remove: () => void;-remove
};

export type TodoListVM = {
  header: {
    newTodoText$: Queryable<string>;
    updateNewTodoText: (text: string) => void;-updateNewTodoText
    addTodo: () => void;-addTodo
  };
  itemList: {
    items$: Queryable<TodoItemVM[]>;
  };
  footer: {
    incompleteDisplayText$: Queryable<string>;
    currentFilter$: Queryable<"all" | "active" | "completed">;
    showAll: () => void;-showAll
    showActive: () => void;-showActive
    showCompleted: () => void;-showCompleted
    clearCompleted: () => void;-clearCompleted
  };
  reset: () => void;
};

const createID = (name: string) => `${name}_${nanoid(12)}`;
export function createTodoListScope(store: Store<typeof schema>): TodoListVM {
  const currentFilter$ = computed((get) => get(uiState$).filter, { label: "filter" });
  const newTodoText$ = computed((get) => get(uiState$).newTodoText, { label: "newTodoText" });

  const createTodoItemVM = memoFn((id: string): TodoItemVM => {
const completed$ = queryDb(
tables.todos.select("completed").where({ id }).first({ behaviour: "error" }), { label: "todoItem.completed", deps: [id] },
);
const text$ = queryDb(tables.todos.select("text").where({ id }).first({ behaviour: "error" }), {
label: "todoItem.text", deps: [id],
}); return { key: id, completed$, text$, toggleCompleted: () => { emitFunctionCall("todo-toggleCompleted", { args: [] }); store.commit(store.query(completed$) ? events.todoUncompleted({ id }) : events.todoCompleted({ id })); }, remove: () => { emitFunctionCall("todo-remove", { args: [] }); store.commit(events.todoDeleted({ id, deletedAt: new Date() })); }, }; }); const visibleTodosQuery = (filter: "all" | "active" | "completed") => queryDb( () => tables.todos.where({ completed: filter === "all" ? undefined : filter === "completed", deletedAt: { op: "=", value: null }, }), { label: "visibleTodos", map: (rows) => rows.map((row) => createTodoItemVM(row.id)), deps: [filter], }, );
const visibleTodos$ = computed(
(get) => { const filter = get(currentFilter$); return get(visibleTodosQuery(filter)); }, { label: "visibleTodos" },
); const incompleteCount_$ = queryDb(tables.todos.count().where({ completed: false, deletedAt: null }), { label: "incompleteDisplayText", }); const incompleteDisplayText$ = computed( (get) => `${get(incompleteCount_$)} ${pluralize(get(incompleteCount_$), "item", "items")} left`, ); return { header: { newTodoText$, updateNewTodoText: (text: string) => { emitFunctionCall("todo-updateNewTodoText", { args: [text] }); store.commit(events.uiStateSet({ newTodoText: text })); }, addTodo: () => { emitFunctionCall("todo-addTodo", { args: [] }); const newTodoText = store.query(newTodoText$).trim(); if (newTodoText) { store.commit( events.todoCreated({ id: createID("todo"), text: newTodoText }), events.uiStateSet({ newTodoText: "" }), // update text ); } }, }, itemList: { items$: visibleTodos$, }, footer: { incompleteDisplayText$, currentFilter$, showAll: () => { emitFunctionCall("todo-showAll", { args: [] }); store.commit(events.uiStateSet({ filter: "all" })); }, showActive: () => { emitFunctionCall("todo-showActive", { args: [] }); store.commit(events.uiStateSet({ filter: "active" })); }, showCompleted: () => { emitFunctionCall("todo-showCompleted", { args: [] }); store.commit(events.uiStateSet({ filter: "completed" })); }, clearCompleted: () => { emitFunctionCall("todo-clearCompleted", { args: [] }); store.commit(events.todoClearedCompleted({ deletedAt: new Date() })); }, }, reset: () => store.commit(events.stateReset({}), events.uiStateSet({ newTodoText: "", filter: "all" })), }; }

Wiring the View Model to React#

The Root component sets up the LiveStore provider and creates the view model:

import { LiveStoreProvider, useStore } from "@livestore/react";
import { createContext, useContext, useMemo } from "react";

const TodoVMContext = createContext<TodoListVM | null>(null);

export const useTodoVM = () => {
  const vm = useContext(TodoVMContext);
  if (!vm) throw new Error("useTodoVM must be used within TodoVMProvider");
  return vm;
};

const TodoVMProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const { store } = useStore();
  const vm = useMemo(() => createTodoListScope(store), [store]);
  return <TodoVMContext.Provider value={vm}>{children}</TodoVMContext.Provider>;
};

export const App: React.FC = () => (
  <LiveStoreProvider
    schema={schema}
    adapter={adapter}
    renderLoading={(state) => <div>Loading LiveStore ({state.stage})...</div>}
  >
    <TodoVMProvider>
      <section className="todos">
        <Header />
        <MainSection />
        <Footer />
      </section>
    </TodoVMProvider>
  </LiveStoreProvider>
);

The key concept is that all components share the same view model instance through React Context. The view model is created once when the store is initialized, and components subscribe to reactive queries that automatically re-render when data changes.

Key patterns:

  • Single source of truth: The LiveStore holds all application state
  • Reactive queries: Components use Queryable<T> to subscribe to data
  • Action methods: View model exposes methods that commit events to the store
  • Separation of concerns: Business logic lives in the view model, not in components