# miniplex: an ECS where the entities are ordinary JavaScript objects

> miniplex is a MIT licensed TypeScript entity component system for games that keeps entities as plain objects, has no built-in notion of systems or scheduler, and indexes queries archetypally. It ships with React bindings, and the last push was on 2026-04-05, so the package versions on npm are older than the code in the repository.

**hmans/miniplex** — A 👩‍💻 developer-friendly entity management system for 🕹 games and similarly demanding applications, based on 🛠 ECS architecture.

- Repository: https://github.com/hmans/miniplex
- Stars: 1,061 · Forks: 45
- Language: TypeScript
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/hmans-miniplex

## One store, one entity type, components as properties

miniplex is an entity management system for games and similarly demanding applications, built on the Entity Component System pattern, and the README describes it as the gentle game entity manager.

The starting point is a contrast with the usual approach. Instead of creating separate buckets for different types of entity, such as asteroids, enemies, pickups and the player, you throw all of them into a single store, describe their properties through components, and then write code that performs updates on entities with specific component configurations.

The type of an entity is declared in TypeScript, and the declaration is what carries the optionality:

```ts
type Entity = {
  position: { x: number; y: number }
  velocity?: { x: number; y: number }
  health?: {
    current: number
    max: number
  }
  poisoned?: true
}
```

A world is then created for that type, and entities are added to it:

```ts
const world = new World<Entity>()

const player = world.add({
  position: { x: 0, y: 0 },
  velocity: { x: 0, y: 0 },
  health: { current: 100, max: 100 }
})
```

The optional properties are the interesting part of the type. An entity that has velocity is a moving thing, and an entity that does not have velocity is not, and the type system encodes that distinction rather than leaving every entity with a nullable field that has to be checked at every use. That is the practical difference between an ECS used as a design and an ECS used as a database with extra steps.

The package is MIT licensed, the primary language is TypeScript, and there is a separate React bindings package, miniplex-react, on npm.

## Queries are archetypal, and they compose

Entity queries are performed through archetypal queries, with individual queries indexing and holding their results. The mechanics are visible in the example.

A query is created with world.with, taking the names of the components that must be present:

```ts
const queries = {
  moving: world.with("position", "velocity"),
  health: world.with("health"),
  poisoned: queries.health.with("poisoned")
}
```

Note the third line. A query is derived from another query by adding a component, which is the archetypal idea in practice. queries.health is every entity that has a health component, and queries.poisoned is the subset of those that also has the poisoned component. The two are not independent lists that have to be kept in sync, they are defined in terms of each other.

A query is then iterable and yields entities with the relevant components already destructured, which is what makes the system functions in the example read so cleanly:

```ts
function moveSystem() {
  for (const { position, velocity } of queries.moving) {
    position.x += velocity.x
    position.y += velocity.y
  }
}
```

The type of the destructured entity is narrowed to what the query guarantees, so velocity is not optional inside the loop even though the entity type declared it as optional. The query is the thing that does the narrowing, and that is the mechanism that makes TypeScript in an ECS worth the effort.

The archetype model also means the cost profile is predictable. An entity is in the archetype defined by exactly which components it has, and queries are indexed by archetype, so a query that matches a small archetype is fast regardless of how many entities exist in the world.

## No built-in systems and no scheduler, by design

This is the decision that most distinguishes miniplex from other ECS libraries, and the README is explicit that it is a decision rather than an omission.

Unlike the majority of ECS libraries, miniplex does not have any built-in notion of systems, and does not perform any of its own scheduling. The stated reasoning is that your project will likely already have an opinion on how to schedule code execution, informed by whatever framework you are using, and instead of providing its own and potentially conflicting setup, miniplex will fit into the one you already have.

In practice, a system is a plain function that operates on the world:

```ts
function healthSystem() {
  for (const entity of queries.health) {
    if (entity.health.current <= 0) {
      world.remove(entity)
    }
  }
}
```

And you call it however your project calls things. The README lists setInterval, requestAnimationFrame, useFrame, or a custom ticker implementation, and that list is the whole scheduling story. If your game already has a game loop, miniplex has nothing to add to it and nothing to fight with.

The trade-off is real. A library that schedules for you gives you a defined order of execution and a place to put per-frame work. A library that does not gives you total freedom and total responsibility, and if you end up writing your own scheduler you have written the part of an ECS that a batteries-included library would have given you.

For a project in React, useFrame is the interesting entry in that list, since it ties the loop to the render cycle rather than to a timer that can drift out of sync with the frame rate.

## Entities are plain objects, components are properties

The headline difference from other ECS libraries is stated plainly: entities are just plain JavaScript objects, and components are just properties on those objects.

Component data can be anything you need, from primitive values to entire class instances, or even entire reactive stores. That last option, pointing at the author's own statery library, is a hint about the design. The library is not trying to be an opinionated data layer, it is trying to be a query and iteration mechanism that gets out of the way.

The related point is that miniplex does not expect you to programmatically declare component types before using them. In a TypeScript project you provide a type describing your entities, and miniplex provides edit-time and compile-time type hints and safety on top of that. The README also notes that you can write classes and use their instances as entities.

That is a different starting point from a library that hands you a component registration API. There is no registry, no schema declaration step, and no component factory. You describe the shape of an entity in a type, and that is the entire declaration.

The cost of the approach is that the library is not in a position to optimise storage the way a data-oriented ECS does, because your entities are objects whose layout the JavaScript engine decides. For a browser game with a few thousand entities, that is the right trade. For a simulation with a hundred thousand entities updated every frame, the overhead of object property access is a real cost, and this library is not aiming at that case.

## Reactive queries and the two operations that change the world

Queries are not just iteration targets, they are observable, which is what makes the library usable in a UI framework.

The example subscribes to entities appearing in a query:

```ts
queries.poisoned.onEntityAdded.subscribe((entity) => {
  console.log("Poisoned:", entity)
})
```

So when an entity gains the poisoned component and starts matching the query, the subscriber fires. That is the hook a React component uses to subscribe to a slice of the world and re-render when it changes, and it is the reason the React bindings are a separate package rather than something baked in.

The mutation API is small, and the example shows both halves. Adding a component to an existing entity is done with world.addComponent:

```ts
function poison(entity: With<Entity, "poisoned">) {
  world.addComponent(entity, "poisoned", true)
}
```

And removing an entity from the world is world.remove(entity), as in the healthSystem function above. Those two operations are the ones that change an entity's archetype membership, and therefore the ones that make queries change their membership, and therefore the ones that fire the subscriptions.

The type signature in the example is worth a second look. The poison function takes an entity typed as With<Entity, "poisoned">. The With helper type describes an entity that is known to have the named component, so inside the function the code can rely on that, and the world is closed over rather than passed in as a parameter. It is a small piece of type-level ergonomics that makes the system functions read like ordinary functions instead of a framework API.

## The monorepo, the tooling, and the version gap

The repository is a pnpm workspace with a preconstruct-based build, and the top level tells you how the project is organised.

The root package is named miniplex-project, is marked private, and its scripts are workspace-level commands. The build is preconstruct build, the dev mode is preconstruct dev, and the postinstall runs preconstruct dev so that the packages point at their source rather than a built artefact during development. The preconstruct configuration names packages/* as the package set, so the two published packages live in that directory.

There is also a demo:react script and a demo:vanilla script, each of which runs the dev build first and then the demo, and the package.json declares a stackblitz start command of pnpm demo:react, so the React example can be opened in a browser from the repository. The apps/ directory holds those demos, and the examples in the README are consistent with a project that expects you to try it in a running app rather than in a console.

Testing is Jest with ts-jest and the jsdom environment, with @testing-library/react and jest-dom for the React bindings, and the CI script runs preconstruct validate, then the build, then the tests. Releases go through changesets, with changeset version and changeset publish, and the ci:version and ci:release scripts wire that into CI. There is a typedoc dependency for the API documentation, a.changeset/ directory, a CHANGELOG.md, and a mise.toml for tool version management.

The version gap is the thing to note. The published releases are miniplex 2.0.0 and miniplex-react 2.0.1 and 2.0.0, all from July 2023, while the last push to the repository was on 2026-04-05. The repository is not archived, but the npm packages have not been republished since then. Anyone adopting this should decide whether they want the published 2.0.1 or the source tree, and the answer determines which documentation applies.

## Conclusion

miniplex is a good fit for a TypeScript game or simulation that wants ECS structure without a framework, especially one in a React renderer where the React bindings and the store model line up, because the entities stay ordinary objects and the library never schedules your code; it is the wrong fit if you want a batteries-included engine with built-in systems, parallel processing or automatic scheduling, and before you adopt it you should check the version you install, since the published packages are from July 2023 while the last push to the repository was on 2026-04-05, which means the npm release and the source tree are not the same thing.

## FAQ

### What is miniplex and how does it model entities?

It is a TypeScript entity management system for games and similar applications. Entities are plain JavaScript objects, components are properties on those objects, and all entities live in a single store instead of separate buckets per type, with queries selecting entities by their component configuration.

### Does miniplex have its own scheduler or system loop?

No, and that is deliberate. The library has no built-in notion of systems and performs no scheduling of its own, so systems are plain functions you call from setInterval, requestAnimationFrame, useFrame or your own ticker, which lets it fit into whatever framework you already use.

### How do miniplex queries work?

Through archetypal queries, created with world.with and the names of the required components. A query can be derived from another by adding a component, as in queries.health.with("poisoned"), and iterating a query yields entities whose components are typed as present rather than optional.

### Does miniplex have React bindings?

Yes, in a separate package called miniplex-react on npm, alongside the core miniplex package. The core library works in any environment, and queries expose an onEntityAdded subscription that the React bindings build on.

### What licence is miniplex released under?

MIT, with the licence text in LICENSE.md at the repository root. The published packages are miniplex 2.0.0 and miniplex-react 2.0.1 from July 2023, and the last push to the repository was on 2026-04-05.

## Sources

- [hmans/miniplex on GitHub](https://github.com/hmans/miniplex)
- [Issues](https://github.com/hmans/miniplex/issues)
- [License: MIT](https://github.com/hmans/miniplex/blob/main/LICENSE)
- [README](https://github.com/hmans/miniplex/blob/main/README.md)
- [Releases](https://github.com/hmans/miniplex/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/hmans-miniplex
