| name | koota |
| description | Real-time ECS state management for TypeScript and React. Use when the user mentions koota, ECS, entities, traits, queries, or building data-oriented applications. |
Koota ECS
Koota manages state using entities with composable traits.
Glossary
- Entity - A unique identifier pointing to data defined by traits. Spawned from a world.
- Trait - A reusable data definition. Can be schema-based (SoA), callback-based (AoS), or a tag.
- Relation - A directional connection between entities to build graphs.
- World - The context for all entities and their data (traits).
- Archetype - A unique combination of traits that entities share.
- Query - Fetches entities matching an archetype. The primary way to batch update state.
- Action - A discrete, synchronous data mutation (create, update, destroy). Reusable from any call site.
- System - A reactive orchestrator that observes state changes and coordinates work, including async workflows. Runs in the frame loop or event callbacks.
Design Principles
Data-oriented
Behavior is separated from data. Data is defined as traits, entities compose traits, and systems mutate data on traits via queries. See Basic usage for a complete example.
Composable systems
Design systems as small, single-purpose units rather than monolithic functions that do everything in sequence. Each system should handle one concern so that behaviors can be toggled on/off independently.
function applyVelocity(world: World) {}
function applyGravity(world: World) {}
function applyFriction(world: World) {}
function syncToDOM(world: World) {}
function updatePhysicsAndRender(world: World) {
}
This enables feature flags, debugging (disable one system to isolate issues), and flexible runtime configurations.
Decouple view from logic
Separate core state and logic (the "core") from the view ("app"):
- Run logic independent of rendering
- Swap views while keeping state (2D ↔ 3D)
- Run logic in a worker or on a server
Prefer traits + actions over classes
Prefer not to use classes to encapsulate data and behavior. Use traits for data and actions for behavior. Only use classes when required by external libraries (e.g., THREE.js objects) or the user prefers it.
Directory structure
If the user has a preferred structure, follow it. Otherwise, use this guidance: the directory structure should mirror how the app's data model is organized. Separate core state/logic from the view layer:
- Core - Pure TypeScript. Traits, systems, actions, world. No view imports.
- View - Reads from world, mutates via actions. Organized by domain/feature.
src/
├── core/ # Pure TypeScript, no view imports
│ ├── traits/
│ ├── systems/
│ ├── actions/
│ └── world.ts
└── features/ # View layer, organized by domain
Files are organized by role, not by feature slice. Traits and systems are composable and don't map cleanly to features.
For detailed patterns and monorepo structures, see references/architecture.md.
Trait types
| Type | Syntax | Use when | Examples |
|---|
| SoA (Schema) | trait({ x: 0 }) | Simple primitive data | Position, Velocity, Health |
| AoS (Callback) | trait(() => new Thing()) | Complex objects/instances | Ref (DOM), Keyboard (Set) |
| Tag | trait() | No data, just a flag | IsPlayer, IsEnemy, IsDead |
Trait naming conventions
| Type | Pattern | Examples |
|---|
| Tags | Start with Is | IsPlayer, IsEnemy, IsDead |
| Relations | Prepositional | ChildOf, HeldBy, Contains |
| Trait | Noun | Position, Velocity, Health |
Relations
Relations build graphs between entities such as hierarchies, inventories, targeting.
import { relation, trait } from 'koota'
const ChildOf = relation({ autoDestroy: 'orphan' })
const Contains = relation({ store: { amount: 0 } })
const Targeting = relation({ exclusive: true })
const parent = world.spawn()
const child = world.spawn(ChildOf(parent))
const gold = world.spawn()
const silver = world.spawn()
const inventory = world.spawn(Contains(gold), Contains(silver))
const children = world.query(ChildOf(parent))
const allChildren = world.query(ChildOf('*'))
const targets = inventory.targetsFor(Contains)
const IsRare = trait()
silver.add(IsRare)
const rareInventories = world.query(Contains(IsRare))
const items = inventory.targetsFor(Contains)
const target = child.targetFor(ChildOf)
For detailed patterns, traversal, ordered relations, and anti-patterns, see references/relations.md.
Basic usage
import { trait, createWorld } from 'koota'
const Position = trait({ x: 0, y: 0 })
const Velocity = trait({ x: 0, y: 0 })
const IsPlayer = trait()
const world = createWorld()
const player = world.spawn(Position({ x: 100, y: 50 }), Velocity, IsPlayer)
world.query(Position, Velocity).updateEach(([pos, vel]) => {
pos.x += vel.x
pos.y += vel.y
})
Entities
Entities are unique identifiers that compose traits. Spawned from a world.
const entity = world.spawn(Position, Velocity)
entity.get(Position)
entity.set(Position, { x: 10 })
entity.add(IsPlayer)
entity.remove(Velocity)
entity.has(Position)
entity.destroy()
Entity IDs
An entity is internally a number packed with entity ID, generation ID (for recycling), and world ID. Safe to store directly for persistence or networking.
entity.id()
entity
Typing
Use TraitRecord to get the type that entity.get() returns
type PositionRecord = TraitRecord<typeof Position>
Queries
Queries fetch entities matching an archetype and are the primary way to batch update state.
world.query(Position, Velocity).updateEach(([pos, vel]) => {
pos.x += vel.x
pos.y += vel.y
})
const data: Array<{ x: number; y: number }> = []
world.query(Position, Velocity).readEach(([pos, vel]) => {
data.push({ x: pos.x, y: pos.y })
})
const player = world.queryFirst(IsPlayer, Position)
world.query(Position, Not(Velocity))
world.query(Or(IsPlayer, IsEnemy))
Prefer updateEach/readEach over for...of + entity.get() for data-bearing queries. readEach still gives you the entity as the second argument.
Note: updateEach/readEach only return data-bearing traits (SoA/AoS). Tags, Not(), and relation filters are excluded:
world.query(IsPlayer, Position, Velocity).updateEach(([pos, vel]) => {
})
For tracking changes, caching queries, and advanced patterns, see references/queries.md.
React integration
Imports: Core types (World, Entity) from 'koota'. React hooks from 'koota/react'.
Change detection: entity.set() and world.set() trigger change events that cause hooks like useTrait to rerender. For AoS traits where you mutate objects directly, manually signal with entity.changed(Trait).
For React hooks and actions, see references/react-hooks.md.
For component patterns (App, Startup, Renderer, view sync, input), see references/react-patterns.md.
Runtime
Systems query the world and update entities. Run them via frameloop (continuous) or event handlers (discrete).
For systems, frameloop, event-driven patterns, and time management, see references/runtime.md.