--- title: "Building a World State (react-jotai)" date: "2025-10-15" draft: true --- # 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 ## 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: ```typescript 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; completed$: Queryable; toggleCompleted: () => void;-toggleCompleted remove: () => void;-remove }; export type TodoListVM = { header: { newTodoText$: Queryable; updateNewTodoText: (text: string) => void;-updateNewTodoText addTodo: () => void;-addTodo }; itemList: { items$: Queryable; }; footer: { incompleteDisplayText$: Queryable; 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): 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$ = /* #fold */ queryDb( tables.todos.select("completed").where({ id }).first({ behaviour: "error" }), { label: "todoItem.completed", deps: [id] }, ); const text$ = /* #fold */ 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$ = /* #fold */ 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: ```tsx import { LiveStoreProvider, useStore } from "@livestore/react"; import { createContext, useContext, useMemo } from "react"; const TodoVMContext = createContext(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 {children}; }; export const App: React.FC = () => ( Loading LiveStore ({state.stage})...} >
); ``` 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` 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