--- title: "Entity Component UI" description: "For complex UIs, we break view models into separate components." date: "2025-10-17" draft: true --- # Entity Component UI {% internal_meta(title="Raw") %} Hi {% internal_meta(title="Anchors") %} TODO: translate the raw thoughts into initial anchors. Controversial thesis: You should split view from business logic with a type-first, fine-grained reactive View Model Interface that lives outside the view lifecycle. - This makes complex UIs testable, predictable, and easier to co-develop (including with LLMs). Hero visual ideas: - Running UI, View Model interface, and test cases in three panes - Could be a simple text input with autocomplete/hints at the bottom **Inspired by** the composability of the Entity-Component-System (ECS) pattern, popularized in game development, and the testability of the Model-View-View-Model (MVVM) pattern, popularized in Swift, Flutter, and Xamarin. **Analogies**: Entity ID = UID; System = WorldState.Plugin **Primary goals**: (unit) testability, composability, observability. **Characteristics**: - Plugins can be tested in complete isolation of each other (e.g. SpatialNav). - Source code clearly shows what components each plugin contributes and depends on. - Central World Store is highly inspectable by being a simple lookup from UIDs to component values. - Reactivity is managed entirely through jotai atoms ## Context Our WorldState architecture is inspired by Entity Component System (ECS) patterns from game development, adapted for dense UI implementations. It decouples behavior, data, and UI rendering across multiple layers. This is especially useful in large-scale or collaborative applications that manage complex data flows–like CRDT-based documents, hierarchical data, or multiple concurrent user sessions. ## Key Concepts ### Entities - **What**: Unique identifiers representing distinct objects in the UI or application (e.g., modules, variables, workspace info, user accounts, etc.). - **Why**: Entities let us compose data and behavior in a flexible, modular way. ### Components - **What**: Reusable pieces of functionality (with their own states/atoms) that can be attached to entities. - **Why**: Components let you attach specialized behavior (e.g., focusable, draggable, collapsable) to any entity without duplicating logic. ### Uniques - **What**: Singleton objects that exist independently of entities (e.g., global settings, user session state). - **Why**: Uniques allow for truly global or application-wide states that any part of the system can reference. ### Entity Map / WorldState - **What**: A central registry that manages entities, components, and uniques. - **Why**: The WorldState orchestrates creation, updates, and disposal, ensuring a single source of truth. ### Plugins - **What**: Extension modules that respond to entity creation, provide derived components, add global uniques, or enforce constraints. - **Why**: Plugins allow building feature layers (like a tree, a grid, or specialized editing behaviors) without entangling the core system logic. ## Benefits ### Composability - You can mix and match components to form new behaviors without duplicating code. - Example: Attaching `CLabel` and `CFocusable` to an entity to make it both labeled and keyboard-focusable. ### Type Safety - The system's use of TypeScript generics ensures correct composition of entities and components. - You get compile-time verification that the right components exist on an entity. ### UI Framework Agnostic - The "ECS"-like logic (entities, components, plugins) is decoupled from rendering (e.g. React). ### Centralized State Management with Jotai - Components store data in Jotai atoms, which are known for their straightforward and granular reactivity. - This fosters predictable state updates and fine-grained performance optimizations. ### Easy Testing - Components can be tested in isolation (like microservices). - You can directly test reactivity, plugin interactions, and more, all without hooking up any UI. ## Example Usage ### Defining a Module Entity ```typescript // Components export class CEditability extends World.Component("editability")< CEditability, { canBeEditedAtom: Atom; disabledReasonAtom: Atom } >() {} export class CLabel extends World.Component("label")() {} // Use tags (zero-value components) to help plugins match on entities of a specific type export class CModuleTag extends World.Component("moduleTag")() {} // Entity export class ModuleEntity extends World.Entity("ModuleEntity", { // Required initial components components: [CModuleTag, CEditability, CLabel], // Components expected to be provided by plugins componentsProvidedByPlugins: [], })() {} ``` ### Creating and Using the Entity in the WorldState ```typescript // 1. Create a world with or without plugins const world = new WorldStateImpl({ store, pool, plugins: [] }); // 2. Add a new module entity const moduleUID = world.addEntity(World.uid(null, null, "module-1"), ModuleEntity, { moduleTag: CModuleTag.of({}), editability: CEditability.of({ canBeEditedAtom: atom(true), disabledReasonAtom: atom(null), }), label: CLabel.of({ textEditor: { /* ...some editor instance... */ }, }), }); // 3. Retrieve and update const labelAtom = world.getComponentAtom(moduleUID, CLabel); store.set(labelAtom, { textEditor: { /* updated editor data */ }, }); ``` ### Derived Components via a Plugin ```typescript // For any entity with CEditability, automatically add a CDisableable class CDisableable extends World.Component("disableable") }>() {} const disablePlugin = { name: "disable-plugin", setup(build: World.WorldStateBuild) { build.onEntityCreated( { // The plugin can match _any_ entity with CEditability, as opposed to only ModuleEntities requires: [CEditability], provides: [CDisableable], }, (uid, { editability }) => ({ disableable: CDisableable.of({ isDisabledAtom: atom((get) => !get(editability.canBeEditedAtom)), }), }), ); }, }; ``` ## Best Practices ### Keep Components Small and Focused Each component should do one job. For example, `CFocusable` for keyboard focus logic, `CTreeMovable` for drag/move logic. ### Prefer Composition Over Inheritance ECS fosters horizontal composition. Attach more components to get new behavior, rather than a deep inheritance hierarchy. ### Use Jotai's `` or Minimal Hooks If you use React, `` or minimal hooks reduce extraneous re-renders. For non-React frameworks, the concept is similar: subscribe only to the atoms you need. ```tsx const labelAtom = world.getComponentAtom(moduleUID, CLabel); // if the atom is a primitive, you can render it directly as text return ; // or use it with a render function return {(label) => {label?.text}}; // depends on the component... return {(label) => label.mount(elt)} />}; ``` ### Test in Isolation Since WorldState logic is framework-agnostic, you can test the business logic of each component or plugin thoroughly without rendering UI. ### Leverage the ECS for Concurrency CRDT merges, multiple user sessions, or real-time data streams can be managed more cleanly by hooking them into WorldState components or uniques, instead of scattering logic throughout the code. ## Conclusion Our WorldState approach (powered by ECS) provides a robust, modular, and testable foundation for building complex UI and application logic. By separating data (via Jotai atoms), behavior (via components), and global singletons (via uniques), we can scale our application functionality while keeping each layer maintainable and comprehensible.