Skip to content

Question: Suspense causes zombie entities in custom renderer due premature creation #33324

Open
@simonedevit

Description

@simonedevit

Hi, I’m reaching out to ask for help regarding an issue I’ve encountered when using Suspense with a custom React renderer that integrates with Babylon.js.

Issue

I'm used to create the Babylon.js entities during the render phase, inside createInstance method. However, if React suspends, it may discard the entire tree before ever reaching the commit phase.

The problem is that my createInstance implementation immediately creates and attaches the Babylon entity to the scene graph. So, even though React throws the tree away, the entity stays in the scene and can't be cleaned up leading to "zombie" entities that were never committed but still exist.

This is expected behavior in React, it may build trees speculatively and discard them. So side-effects (like entity creation or mounting into a scene graph) must be deferred to the commit phase, not the render phase.

Attempted workaround

To work around this, I’ve tried delaying Babylon.js entity creation until I’m sure React has committed the tree, by moving logic into appendInitialChild, appendChildToContainer, appendChild, insertBefore, insertInContainerBefore.

appendInitialChild(parentInstance, child) {
   parentInstance.children.push(child);
   mountBabylonEntity(parentInstance, child);
}

appendChild(parentInstance, child) {
   parentInstance.children.push(child);
   mountBabylonEntity(parentInstance, child);
},

appendChildToContainer(container, child) {
   container.children.push(child);
   mountBabylonEntity(container, child);
},
 
insertBefore(parentInstance, child, beforeChild) {
   const index = parentInstance.children.findIndex(item => item.entity!.uniqueId === beforeChild.entity!.uniqueId);
   parentInstance.children.splice(index, 0, child);
   mountBabylonEntity(parentInstance, child);
},

insertInContainerBefore(container, child, beforeChild) {
   const index = container.children.findIndex(item => item.entity!.uniqueId === beforeChild.entity!.uniqueId);
   container.children.splice(index, 0, child);
   mountBabylonEntity(container, child);
}

Then i recursively mount the child entities only when React appears to be finalizing the tree.

// create recursively the Babylon.js instances
function mountBabylonEntity(parent: Instance | RootContainer, child: Instance, isReady: boolean) {

    const isParentRootContainer = parent.name === ROOT_IDENTIFIER; // true when the parent is a root container

    if (isParentRootContainer || isReady){
       finalTree.status = true;
       // generate child Babylon.js entity
       // generate parent Babylon.js entity
     
      for (const currentChild of child.children){
         mountBabylonEntity(child, currentChild, true);
      }
    }
}

This effectively delays creation until React commits the current child to container (appendChildToContainer is called, maybe wrong assumption). This kind of works, but I'm unsure whether it's a good approach or if I'm misunderstanding how commit works in custom renderers.

Code reference

You can see the relevant reconciler implementation here: https://github.com/simonedevit/reactylon/blob/delay-instance-creation/packages/library/src/reconciler.ts#L153.

Versions

react: ^19
react-reconciler: 0.31.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    Status: UnconfirmedA potential issue that we haven't yet confirmed as a bug

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions