Skip to content

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.

NeedAPI
Cascade an explicit component removalworld.onComponentRemoving()
Read data before a component is removedworld.onComponentRemoved()
Release an external object owned by a componentworld.onComponentRemoved()
Release per-entity external stateworld.onEntityDespawn()
Observe a replacement or unload transactionworld.onReset()
React to combat deathExplicit gameplay state before despawn
Detect initial query membershipQuery filter Spawned or Entered

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:

PropertyDescription
entityFrozen { id, uid?, registryId, index, generation } identity
reasonA RemovalReason value
commandsLifecycle-safe deferred structural commands

The UID snapshot is present only if it was already materialized when removal began.

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.

ValueMeaning
RemovalReason.ComponentRemoveA component was explicitly removed
RemovalReason.GameplayOrdinary entity despawn, including gameplay bulk despawn
RemovalReason.SceneReplaceExisting state was replaced by a scene load
RemovalReason.SceneUnloadA child scene was unloaded
RemovalReason.WorldUnloadThe 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.

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.

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.