CLI tool
pmndrs/koota avatar
pmndrs/koota

Koota, an entity component system for React and real-time apps

🌎 Performant real-time state management for React and TypeScript

759 stars43 forksTypeScriptISC

At a glance

What is it?
pmndrs/koota is a state management library built on the entity component system pattern, aimed at real-time applications, games and XR experiences rather than at games alone. Entities hold traits, queries batch over them, and the koota/react entry point makes trait data readable from components, with relations and automatic destruction of orphans for hierarchies.
Who is it for?
Adopt Koota if your React application has thousands of entities with per-entity state, such as a game, a simulation or an XR scene, and you want that state to live outside the component tree. Skip it if you have a handful of components with ordinary state, because an ECS adds indirection without buying anything there.
Can I use it commercially?
Yes. ISC is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 5 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What the library is and who it targets

Koota is an ECS-based state management library optimised for real-time apps, games and XR experiences, and the README says you can use as much or as little of it as you need.

The interesting part of that positioning is the first word in the list. The entity component system pattern comes from game development, where thousands or millions of entities need the same data updated every frame and a component tree is the wrong shape for it. Koota takes that pattern and offers it to React developers, and the deliberate inclusion of real-time apps and XR alongside games says the authors think the pattern applies more widely than to games.

That is a defensible position and it has a specific technical claim behind it. In a typical React application, state lives in the component, and a hundred similar objects means a hundred components each holding their own copy. In an ECS, state lives in typed stores keyed by trait, and a query over all entities with a given trait is a tight loop over contiguous data. The library exists to give you that shape inside a React application, and the React bindings are a view onto it rather than the core of it.

The package is installed from npm with npm i koota. The project also publishes an official AI agent skill that can be installed with npx skills add pmndrs/koota, which is the same pattern of shipping procedural knowledge alongside a library that you have seen in other projects in this ecosystem.

The releases carry names, v0.6.6 The Lost Release, v0.6.5 Rachel's Song and v0.6.4 Agora, and the last one shipped in August 2026 with a commit the same day as the current version, so the project is active. The repository is under the pmndrs organisation, which is the React and three.js ecosystem group, and the topics list data-oriented design alongside ECS and React.

Traits, entities and the query loop

The building block is a trait, which represents a slice of data with a specific meaning. A trait with default values is the simple case:

js
import { trait } from 'koota'

// Basic trait with default values
const Position = trait({ x: 0, y: 0 })
const Velocity = trait({ x: 0, y: 0 })

A trait can also take a callback for the initial value, and the README warns that it must be an object:

js
// Trait with a callback for initial value
const Mesh = trait(() => new THREE.Mesh())

A trait with no data at all is a tag, which is how you mark entities for membership in a query without carrying anything.

Entities are spawned into a world, and adding traits to them gives them content:

js
import { createWorld } from 'koota'

const world = createWorld()

const player = world.spawn(Position, Velocity)
// Initial values can be passed in to the trait by using it as a function
const goblin = world.spawn(Position({ x: 10, y: 10 }), Velocity, Mesh)

The second form is worth pausing on. Passing a trait as a function supplies initial values for that entity only, so two entities with the same trait can hold different data. That is what makes a trait a template rather than a fixed record.

Queries fetch entities that share traits, and the README calls the matching group an archetype. The update method mutates and writes back to the stores, and it is meant to run in a loop:

js
// updateEach mutates and writes back to stores
// Run this in a loop
world.query(Position, Velocity).updateEach(([position, velocity]) => {
  position.x += velocity.x * delta
  position.y += velocity.y * delta
})

And for read-only work there is a separate method, because mutating in a read path is how you end up with bugs that only appear on some frames:

js
// For read-only operations with no mutation, use readEach
const data = []
world.query(Position, Velocity).readEach(([position, velocity]) => {
  data.push({ x: position.x, y: position.y })
})

The split between updateEach and readEach is the kind of small API decision that says the library has been used in a real-time loop rather than designed on paper.

Reading trait data from React and the actions pattern

The React integration is where the library meets the framework, and it works through a provider and two hooks.

You wrap the application in a WorldProvider and pass the world in. Then useQuery selects entities by trait, and useTrait observes one entity's trait data:

js
import { WorldProvider, useQuery, useTrait } from 'koota/react'

// Wrap your app in WorldProvider
createRoot(document.getElementById('root')!).render(
    <WorldProvider world={world}>
        <App />
    </WorldProvider>
);

function RocketRenderer() {
    // Reactively update whenever the query updates with new entities
    const rockets = useQuery(Position, Velocity)
    return rockets.map((entity) => <RocketView key={entity} entity={entity} />)
}

function RocketView({ entity }) {
    // Observes this entity's position trait and reactively updates when it changes
    const position = useTrait(entity, Position)
    return (
        <div style={{ position: 'absolute', left: position.x ?? 0, top: position.y ?? 0 }}>
        </div>
    )
}

The rendered element in the example is an absolutely positioned div, and the point of the two hooks is that they divide the work. The two hooks have different jobs and it matters. useQuery subscribes to the set of matching entities, so a component re-renders when an entity appears or disappears, and the entities it returns are stable references. useTrait subscribes to the data of one entity's trait, so the component re-renders when that data changes, and the value it returns is the trait data directly, which is why the example reads position.x without any unwrapping.

That division is the thing that makes the library usable in React. A component that renders one entity does not need to know about the query, and a component that renders a list does not need to re-read the trait data itself. The query is for membership and the trait is for values.

Writing state from React is handled separately, and the README is careful to recommend the actions pattern for it:

js
import { createActions } from 'koota'
import { useActions } from 'koota/react'

const actions = createActions((world) => ({
  spawnShip: (position) => world.spawn(Position(position), Velocity),
  destroyAllShips: () => {
    world.query(Position, Velocity).forEach((entity) => {
      entity.destroy()
    })
  },
}))

The actions are defined once against the world, and useActions hands them to a component so that mutations happen in a known place rather than scattered through event handlers. The example spawns three ships in a useEffect on mount and destroys them on cleanup, and the same actions drive a button that destroys everything at once. The alternative shown is to reach for the world directly with useWorld, which works and is what you would do in a non-React context.

The point of the actions pattern is that state lives outside React, so a mutation from a React effect and a mutation from a game loop follow the same path.

Relations, hierarchies and where this fits against the alternatives

Relations extend the model to graphs, and the design decision in them is worth understanding.

A relation is created with the relation function, and one is added to an entity to point at another. The README explains the terminology precisely: the source is the entity that has the relation added and the target is the entity it points to. In child.add(ChildOf(parent)), the child is the source and the parent is the target.

The child knowing about the parent, rather than the other way round, is deliberate. The README says the parent does not need to know about its children, so children care about their parent, which optimises batch queries. A parent with ten thousand children is a common shape, and making the parent hold the list means every operation on a child touches a list that keeps growing, whereas the child-side relation means a query for a parent's children has to scan. The trade is deliberate and the direction is the one that suits the common case.

Relations can carry data like a trait, so a Contains relation with an amount field works, and the data is read with get and written with set.

The autoDestroy option is the part that saves the most code. With the orphan or source option, destroying a target destroys all the sources pointing at it, which is how you clean up a hierarchy when the root goes away. The README's example destroys a parent and the child and grandchild go with it, and world.has(child) returns false afterwards. With the target option the direction reverses, and destroying a container destroys the items inside it.

The comparisons to make are with the alternatives. Redux and Zustand are the mainstream state options, and they are the right answer for an application whose state is a set of named values. They are the wrong answer for thousands of similar objects that all need updating each frame, because the component tree and the reconciliation model both scale with the number of components. Koota is the answer to that specific problem, and the README's own positioning, as much or as little as you need, is an invitation to use it for the parts of your application that look like a game loop.

The comparison with a game engine's own ECS is the other one. If you are writing a game in a framework with an ECS, use that. Koota's case is the application that is not quite a game, where a handful of entities need per-frame updates and a React UI needs to read some of them, and pulling in a full engine for that is disproportionate.

The cost is the same one every ECS has. You are learning a different way to hold state, and the React bindings are the only place the two worlds meet, so a developer has to be comfortable in both. For a team that already thinks in data-oriented design, that is nothing. For a team that does not, it is the cost of the whole thing.

Editorial conclusion

Adopt Koota if your React application has thousands of entities with per-entity state, such as a game, a simulation or an XR scene, and you want that state to live outside the component tree. Skip it if you have a handful of components with ordinary state, because an ECS adds indirection without buying anything there. Verify first by defining one trait, spawning a few entities, and using useTrait in a component to confirm that updates propagate without a manual subscription.

Frequently asked questions

What is Koota and what is it for?

It is an ECS-based state management library optimised for real-time apps, games and XR experiences, and the README says you can use as much or as little of it as you need. It is installed with npm i koota.

What are traits in Koota?

Traits are the building blocks of state, slices of data with specific meanings. A trait can carry default values, take a callback for an initial value which must return an object, or carry no data at all as a tag for query membership.

How does Koota differ from Redux or Zustand?

Redux and Zustand are right for an application whose state is a set of named values, and wrong for thousands of similar objects that all need per-frame updates, because the component tree scales with the number of components. Koota keeps state in typed stores and lets a query batch over entities with the same traits.

What do useQuery and useTrait do in koota/react?

useQuery subscribes to the set of entities matching a set of traits and re-renders when membership changes, returning stable entity references. useTrait observes the data of one entity's trait and re-renders when that data changes, returning the trait data directly.

How do relations handle cleanup in Koota?

The autoDestroy option takes orphan or source, meaning that destroying a target also destroys the sources pointing at it, which is how a hierarchy is cleaned up when its root is destroyed. The target option reverses the direction, so destroying a container destroys the items it contains.

Official sources

  1. Issues
  2. License: ISC
  3. pmndrs/koota on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/pmndrs-koota.svg)](https://hysenlabs.com/projects/pmndrs-koota)

Was this useful?