Multiple instances of Three.js being imported

Hi! First time using Speckle as a developer.

I’ve integrated the viewer into a JS/React project but I get the warning, “THREE.WARNING: Multiple instances of Three.js being imported” and I’m wondering why. I can see it’s correct because my element tree looks like this.

<div id="speckle-container"><canvas data-engine="three.js r140" width="1216" height="1466" style="display: block; width: 608px; height: 733px;"></canvas><canvas data-engine="three.js r140" width="1202" height="1466" style="display: block; width: 601px; height: 733px;"></canvas></div>

I get this wether or not I’m in React’s StrictMode also.

Currently this is how I’ve created the viewer React component if it helps with debugging.

import { useEffect, useRef } from "react";
import {
  Viewer,
  DefaultViewerParams,
  SpeckleLoader,
  UrlHelper,
} from "@speckle/viewer";
import { CameraController, SelectionExtension } from "@speckle/viewer";

const STREAM_URL =
  "https://app.speckle.systems/projects/7591c56179/models/32213f5381";

function SpeckleViewer() {
  const containerRef = useRef(null);
  const viewerRef = useRef(null);

  useEffect(() => {
    const params = DefaultViewerParams;
    params.showStats = false;
    params.verbose = false;

    const viewer = new Viewer(containerRef.current, params);
    viewerRef.current = viewer;

    const run = async () => {
      await viewer.init();
      viewer.createExtension(CameraController);
      viewer.createExtension(SelectionExtension);

      const urls = await UrlHelper.getResourceUrls(STREAM_URL);
      for (const url of urls) {
        const loader = new SpeckleLoader(
          viewer.getWorldTree(),
          url,
          import.meta.env.VITE_SPECKLE_TOKEN
        );
        await viewer.loadObject(loader, true);
      }
    };

    run();
  }, []);

  return <div ref={containerRef} id="speckle-container" />;
}

export default SpeckleViewer;

Thank you!

Also, I can confirm there are no other Three.js modules being imported in my project, just the one from Speckle.

Solved, sort of. Added the gaurd if (viewerRef.current) return at the top of useEffect(). Still however getting the multiple THREE.WARNING.

:waving_hand:Try referencing the viewer instead.


const viewer = useRef<Viewer | null>(null);
	let divRef: HTMLElement | null = null;

	useEffect(() => {
		if (divRef) {
            viewer.current = new Viewer(divRef, DefaultViewerParams):
		}
	}, [divRef]);

	return (
		<div
			ref={(node) => {
				divRef = node;
			}}
			className={'ViewerControl'}
		/>
	);
});

1 Like

Common issue in the JavaScript ecosystem. Usually this can be avoided if library maintainers make the dependency (three.js in this case) a peerDependency, so that the devs using it have to install it manually, but also ensure that there’s only 1 specific version installed.

In this case, however, even if the viewer package would make three.js a peerDependency, there are other indirect depdendencies of the viewer that are still not gonna do that and so the problem isn’t resolved.

In such cases you’re supposed to configure your build tool (vite? rollup? webpack?) to dedupe this dependency to ensure that only 1 version is ever bundled.

I think for Vite this is the setting that must be used - Shared Options | Vite

Not sure about other build tools, but there is pretty much always some kind of config option with dealing with this issue.

Another way to approach this is to dedupe at the node_modules level. In this case it would not be your build tool, but your package manager (npm? yarn?) that needs configuring to avoid multiple instances. Maybe yarn dedupe three.js is enough, or maybe you need to use resolutions to force a specific version and then dedupe it with yarn dedupe.

3 Likes

I think I’ve done that but still the same issue. Thanks, David.

function SpeckleViewer() {
  const { objs } = useStore();
  const containerRef = useRef(null);
  const viewerRef = useRef(null);

  // Setup the Speckle Viewer

  useEffect(() => {
    if (viewerRef.current) return;

    const params = DefaultViewerParams;
    params.showStats = false;
    params.verbose = true;

    const viewer = new Viewer(containerRef.current, params);
    viewerRef.current = viewer;

    const setupViewer = async () => {
      await viewer.init();
      viewer.createExtension(CameraController);
      viewer.createExtension(SelectionExtension);
    };

    setupViewer();
  }, []);
1 Like

Thank you. I tried all of this but no luck either :frowning:

The solutions I outlined should work - either node_modules level dedupe or dedupe in the final app bundle. With either of those correctly configured there just isn’t a way for three.js to run multiple instances.

Can you share your build config and your package.json? What package manager are you using?

1 Like

I’m probably doing it wrong. I’m using npm.

Here’s my Vite config. It’s super empty.

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vite.dev/config/
export default defineConfig({
  plugins: [react()],
})

Here’s my package.json

{
  "name": "lazy-survey",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "@mapbox/mapbox-gl-draw": "^1.5.1",
    "@mapbox/mapbox-gl-geocoder": "^5.1.2",
    "@speckle/viewer": "^2.26.6",
    "@turf/area": "^7.2.0",
    "@turf/projection": "^7.2.0",
    "compute-rhino3d": "^0.13.0-beta",
    "mapbox-gl": "^3.16.0",
    "react": "^19.1.1",
    "react-aria-components": "^1.13.0",
    "react-dom": "^19.1.1",
    "rhino3dm": "^8.17.0",
    "zustand": "^5.0.8"
  },
  "devDependencies": {
    "@eslint/js": "^9.36.0",
    "@types/react": "^19.1.16",
    "@types/react-dom": "^19.1.9",
    "@vitejs/plugin-react": "^5.0.4",
    "eslint": "^9.36.0",
    "eslint-plugin-react-hooks": "^5.2.0",
    "eslint-plugin-react-refresh": "^0.4.22",
    "globals": "^16.4.0",
    "vite": "^7.1.7"
  }
}

Is it simply just because I’m using mapbox-gl in one div (left) which is using Three under the hood and now Speckle (right) which is doing the same? Screenshot attached below.

Thank you,

Aidan

@aidannewsome Doesn’t appear that you’ve set up any of the solutions I outlined.

Try one of these:

Build tool dedupe

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vite.dev/config/
export default defineConfig({
  plugins: [react()],
  resolve: {
    dedupe: ['three']
  }
})

Package level dedupe

{
  "name": "lazy-survey",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "@mapbox/mapbox-gl-draw": "^1.5.1",
    "@mapbox/mapbox-gl-geocoder": "^5.1.2",
    "@speckle/viewer": "^2.26.6",
    "@turf/area": "^7.2.0",
    "@turf/projection": "^7.2.0",
    "compute-rhino3d": "^0.13.0-beta",
    "mapbox-gl": "^3.16.0",
    "react": "^19.1.1",
    "react-aria-components": "^1.13.0",
    "react-dom": "^19.1.1",
    "rhino3dm": "^8.17.0",
    "zustand": "^5.0.8"
  },
  "devDependencies": {
    "@eslint/js": "^9.36.0",
    "@types/react": "^19.1.16",
    "@types/react-dom": "^19.1.9",
    "@vitejs/plugin-react": "^5.0.4",
    "eslint": "^9.36.0",
    "eslint-plugin-react-hooks": "^5.2.0",
    "eslint-plugin-react-refresh": "^0.4.22",
    "globals": "^16.4.0",
    "vite": "^7.1.7"
  },
  "overrides": {
    "three": "0.140.2"
  }
}

The overrides bit will force all installed packages to only use this one specific three.js version. This means that you will also have to manually update the version to the correct one that the viewer package expects, however (currently seems to be 0.140.2)

I understand. I did try your solutions and I would have to force all versions of Three to 0.140. But if I’m using other apps like Mapbox that also use Three and maybe a newer version of Three, won’t doing that potentially break things? In that case I guess this warning is okay…

@aidannewsome The warning strongly implies that you should not have multiple versions of three.js running. Not only because the library is massive and bundling it multiple times is gonna meaningfully impact your app’s performance, but also because it clearly relies on some kind of singleton state that must not be duplicated. Even the three.js docs state this:

So if you’re worried about breaking things, you’re much more likely to break things running multiple instances of three.js than forcing all of your dependencies to rely on the same version instead. I don’t think forcing them to do that should be an issue at all, and if it is - then you should evaluate whether you should stop using incompatible dependencies.

1 Like

If you run npm ls three in your CLI its gonna tell you what all of the installed versions are. They’re probably very close to each other, in which case you can just take the latest one and force that. If they’re many versions apart, to the point where stuff is breaking, then clearly you’re not supposed to use these dependencies together.

1 Like

Thank you, Fabians. That’s good advice. Sorry not a software dev. Just trying to learn. I really appreciate your help.

2 Likes

This is what npm ls three produces in my CLI.

aidannewsome@MacBookPro lazy-survey % npm ls three
lazy-survey@0.0.0 /Users/aidannewsome/dev/personal/lazy-survey
├─┬ @speckle/viewer@2.26.6
│ ├─┬ three-mesh-bvh@0.5.17
│ │ └── three@0.140.2 deduped
│ ├── three@0.140.2 overridden
│ └─┬ troika-three-text@0.52.4
│   ├── three@0.171.0 deduped
│   └─┬ troika-three-utils@0.52.4
│     └── three@0.171.0 deduped
└── three@0.171.0 overridden

And my updated package.json with the package level dedupe (if I’ve done it correctly).

{
  "name": "lazy-survey",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "@mapbox/mapbox-gl-draw": "^1.5.1",
    "@mapbox/mapbox-gl-geocoder": "^5.1.2",
    "@speckle/viewer": "^2.26.6",
    "@turf/area": "^7.2.0",
    "@turf/projection": "^7.2.0",
    "compute-rhino3d": "^0.13.0-beta",
    "mapbox-gl": "^3.16.0",
    "react": "^19.1.1",
    "react-aria-components": "^1.13.0",
    "react-dom": "^19.1.1",
    "rhino3dm": "^8.17.0",
    "three": "^0.171.0",
    "zustand": "^5.0.8"
  },
  "devDependencies": {
    "@eslint/js": "^9.36.0",
    "@types/react": "^19.1.16",
    "@types/react-dom": "^19.1.9",
    "@vitejs/plugin-react": "^5.0.4",
    "eslint": "^9.36.0",
    "eslint-plugin-react-hooks": "^5.2.0",
    "eslint-plugin-react-refresh": "^0.4.22",
    "globals": "^16.4.0",
    "vite": "^7.1.7"
  },
  "overrides": {
    "three": {
      "three": "^0.140.2"
    }
  }
}

I don’t think three needs to be nested, then again I haven’t used npm in a while so if it works (you can double check with npm ls three returning the same version for everything) it’s OK.

Thanks Fabians, and sorry again for all the trouble. I’m a newb. I forced the additional Three package I was using to 0.140.2 to match Speckle and it fixed the warning. I appreciate your help.

Aidan