Removal Lifecycle
Removal callbacks are synchronous lifecycle hooks that run before physical storage removal.
onComponentRemoving() handles explicit structural cascades while the entity is active;
onComponentRemoved() handles external disposal for every removal cause.
Choose the right API
Section titled “Choose the right API”| Need | API |
|---|---|
| Cascade an explicit component removal | world.onComponentRemoving() |
| Read data before a component is removed | world.onComponentRemoved() |
| Release an external object owned by a component | world.onComponentRemoved() |
| Release per-entity external state | world.onEntityDespawn() |
| Observe a replacement or unload transaction | world.onReset() |
| React to combat death | Explicit gameplay state before despawn |
| Detect initial query membership | Query filter Spawned or Entered |
Component removal
Section titled “Component removal”world.onComponentRemoving(component, callback) runs only when the component is explicitly removed.
It receives the active entity, making it the appropriate place to enqueue related structural work:
world.onComponentRemoving(RenderIntent, ({ entity, commands }) => { if (entity.hasComponent(RenderNode)) commands.removeComponent(entity, RenderNode);});It does not run during gameplay despawn, scene replacement, scene unload, or world unload. The event
contains the live entity and lifecycle-safe deferred commands; it has no reason because its cause
is always explicit component removal. For an explicit removal, all onComponentRemoving() callbacks
run before onComponentRemoved() callbacks.
world.onComponentRemoved(component, callback) covers explicit removal, gameplay despawn, scene
replacement, scene unload, and world unload. Its second callback argument is a typed read-only proxy
for the component being removed:
const subscription = world.onComponentRemoved(RenderNode, ({ entity, reason, commands }, node) => { renderer.release(node.handle);
if (reason === RemovalReason.Gameplay) commands.despawn(linkedEntityUid);});
subscription.dispose();Pass { includeTeardown: false } as the third argument when cleanup should run for explicit
component removal and gameplay despawn, but not during scene replacement, scene unload, or world
unload:
world.onComponentRemoved(LinkedNode, ({ commands }, node) => commands.despawn(node.linkedEntity), { includeTeardown: false,});Teardown callbacks are included by default.
The component proxy is live and valid only during the callback. Read it synchronously and never retain
it. The ECS does not clone component data, so writes made during onComponentRemoving() are visible to
the later onComponentRemoved() callback.
If cleanup needs another component on the entity, resolve the frozen identity with
world.get('removalLifecycle').resolveEntity(identity). The resolver uses the world-family registry
directory and verifies registry, index, generation, and numeric entity ID without materializing a
UID. It returns undefined for stale identities.
The callback event contains:
| Property | Description |
|---|---|
entity | Frozen { id, uid?, registryId, index, generation } identity |
reason | A RemovalReason value |
commands | Lifecycle-safe deferred structural commands |
The UID snapshot is present only if it was already materialized when removal began.
Entity despawn
Section titled “Entity despawn”world.onEntityDespawn(callback) runs once before archetype detachment and storage compaction:
world.onEntityDespawn(({ entity, reason }) => { entityRegistrations.delete(entity.id); const uid = entity.uid ?? world.get('removalLifecycle').resolveEntity(entity)?.uid; if (reason === RemovalReason.Gameplay && uid) analytics.record('entity-despawned', uid);});Component-specific onComponentRemoved() callbacks run first for subscribed components present on
the entity. onComponentRemoving() does not run because despawn is not an explicit component
removal. The entity callback runs afterward while live storage remains available.
Removal reasons
Section titled “Removal reasons”| Value | Meaning |
|---|---|
RemovalReason.ComponentRemove | A component was explicitly removed |
RemovalReason.Gameplay | Ordinary entity despawn, including gameplay bulk despawn |
RemovalReason.SceneReplace | Existing state was replaced by a scene load |
RemovalReason.SceneUnload | A child scene was unloaded |
RemovalReason.WorldUnload | The root world was unloaded |
Replacement and unload are teardown boundaries, not gameplay death. Represent death explicitly, allow ordered systems to react while the entity is live, then despawn it in a final system.
world.onReset(callback) runs once at the start of replacement or unload. Use it for transaction-wide
state such as pending network despawns or caches requiring an authoritative rebuild.
Deferred commands
Section titled “Deferred commands”Structural work requested from callbacks must use the event’s commands. Commands execute FIFO after
the current removal operation, preventing recursive removal dispatch. Explicit component cascades
normally originate in onComponentRemoving():
world.onComponentRemoving(RenderIntent, ({ entity, commands }) => { commands.removeComponent(entity, RenderNode);});commands.despawn(), commands.removeComponent(), and commands.addComponent() are supported.
Entity arguments must belong to the world that emitted the removal event. UID despawns retain the
existing registry-local lookup behavior.
Deferred component additions run after the current removal is structurally effective, so a callback
can remove and re-add the same component with new data. Mutation hooks observe the remove before the
insert. Cascades are FIFO and limited to 10,000 deferred commands.
Bulk removal and reset
Section titled “Bulk removal and reset”EntityRegistry.despawnAll() performs ordinary gameplay removal. Internal
EntityRegistry.reset(reason) performs synchronous scene/world replacement or unload and must not use
the gameplay reason.
External resource owners should register lifecycle cleanup beside the resource they own. Teardown must not depend on a later scheduler tick.