Cloning bugs in React Three Fiber

I have encountered several issues when cloning models in React Three Fiber. It is my understanding that it works differently than vanilla Three.js. Let me describe what I am building:

  • I have an array of products, with each product containing a URL the loader can use to load the model for that product, among other things.
  • When a user adds a product to the canvas, it is added to this product array. A user can add more than one of the same product. This is where the issue lies.
  • This products array is mapped to a model component inside the canvas something like this:
  { => (
                      <Model key={product.uniqueID} product={product} isSelectedObject={isSelectedObject(product)} />                

Inside Model if I load the gltf and return a primitive with the object attribute set to gltf.scene, all is well except I cannot create more than one of the same 3D Model on the canvas. For example, if products contains 2 door handles, it’s only going to render one (it is not an issue of data consistency in the array). A solution is to set the object attribute to gltf.scene.clone(), but then trying to do anything with the reference to the primitive is a nightmare. I am also using TransformControls, so I need to attach them to the clone, and do a number of other transformations, all of which work fine when there is no clones. I have also tried cloning once the gltf has loaded, setting the reference to the clone and then adding it to the scene:

  // Clones the GLTF model, adds it to the scene, and syncs its position with the store.
  useEffect(() => {
    if (!gltf) return; // Ensure GLTF is loaded
    if (more than one of same product in array){
      // Clone the GLTF scene for this instance
      const clone = gltf.scene.clone();

      // Apply initial transformations if necessary
      clone.position.set(...(product.position || [0, 0, 0]));

      // Use ref to keep track of the cloned model
      modelRef.current = clone;

      // Add the clone to the scene
    } else {
      modelRef.current = gltf.scene;
  }, [gltf]); // executes when the gltf has fully loaded

(this code is a simplified version)

I’ve taken a number of approaches with varying bugs. These range from things working 90% of the time, but models occasionally seemingly randomly disappearing, or with other approaches, models cloning too much and ending up with dead copies of a mesh on the canvas without a transform attached to it anymore. Can someone point me to an example or share the best practice for cloning in react three fiber? or should I even be cloning? Thanks.

use gltfjsx GitHub - pmndrs/gltfjsx: 🎮 Turns GLTFs into JSX components your model is immutable with this, you can re-use it. this is much cleaner than cloning and the whole scene is under your control, too.

other than that you should not call scene.add/remove, you mount/unmount things in react. also not a good idea to clone in a useeffect after the fact, cloning is not a side-effect but a calculation which you can memoize. as for singling out props like position, you have rest spread, this makes your components flexible. you wouldn’t want to be tied to position, but not scale or something else, or having to extend it every time you need a new prop.

function Model({ url, ...props }) {
  const { scene } = useGLTF(url)
  const clone = useMemo(() => scene.clone(), [scene])
  return <primitive object={clone} {...props} />

<Model url="foo.glb" position={[1, 2, 3]} />
<Model url="foo.glb" scale={4} />
<Model url="foo.glb" position={[1, 2, -3]} rotation={[Math.PI, 0, 0]} />

ps, more strange things:

  • if (!gltf) return; // Ensure GLTF is loaded — the gltf is guaranteed to be loaded with useLoader/useGLTF, it cannot possibly be not loaded
  • // Add the clone to the scene scene.add(clone) most likely your bug, you mutate the scene (which is bad in itself) but you’re missing the cleanup. components don’t run once, they should be resilient to re-render with zero side-effects, yours dumps objects into the scene every time

Yeah I didn’t like the solution. That excerpt was from me recently prototyping a solution to the issue. I do have a cleanup, just not in what I shared. Also the bit about the “if (!gltf) return;” was based off an assumption that the process was asynchronous. gltfjsx probably sounds like what I am looking for so I appreciate the tip.

use gltfjsx or primitive object like above, either is fine. you can only use gltfjsx for models you know, it’s a cli tool. use primitive objects for unknown/remote models.

was based off an assumption that the process was asynchronous

it is. but all loader hooks use react suspense, it stops the component from progressing by literally throwing the loading promise as an exception which react catches. it starts rendering the component again once the promise has listed. that’s why the result is there next line. if the promise fails (loading error etc) the component wouldn’t render but run into the nearest error boundary which you can also catch. suspense frees you from dealing with loading state and errors, which are parental concerns, no individual component should have to deal with that.

This makes more sense then. As an aside, I still think I lack the understanding as to why returning two seemingly different primitives to the canvas loaded via the same URL results in only one rendered. The issue does not happen when loading different models through the same component in the same manner.

because that’s how threejs works.

const { scene } = await gltfLoader.loadAsync("model.glb")

sceneB.add(scene) // it will only be in here, three removed it from sceneA

now imagine you have a component that you mount twice

function Model() {
  const { scene } = useGLTF("model.glb")
  return <primitive object={scene} />

<Model />
<Model /> // three will remove the first instance

if you put one model multiple times into the scene threejs will auto remove. fiber is threejs, it doesn’t change behaviour. useMemo(() => scene.clone(), [scene]) fixes that just like it would in vanilla. gltfjsx isn’t just a fix, it’s a workflow.


Appreciate all the advice. For anyone wondering in the future, I fixed my solution by instead using drei’s Clone and wrapping the TransformControls context around it, as well as making some changes based on what @drcmda has posted here.