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();
  }, []);

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?