Skip to main content
Skip table of contents

Framework Integration (React, Next.js)

The ShapeDiver Viewer is framework-agnostic — it works with vanilla JavaScript, React, Vue, Angular, Next.js, and other frameworks. However, certain frameworks require specific patterns to integrate correctly. This page covers the most common integration patterns.

For a complete React-based application template, see the ShapeDiver React Example on GitHub.


React Integration

Basic Setup with useEffect

The viewer requires a DOM element (canvas) to render into. In React, you must wait for the component to mount before initializing the viewer. Use useEffect with a ref to the canvas:

TYPESCRIPT
import React, { useEffect, useRef } from "react";
import { createViewport, createSession, IViewportApi, ISessionApi } from "@shapediver/viewer";

const ShapeDiverViewer: React.FC = () => {
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    let viewport: IViewportApi | undefined;
    let session: ISessionApi | undefined;

    const init = async () => {
      viewport = await createViewport({
        canvas: canvasRef.current!,
        id: "myViewport",
      });
      session = await createSession({
        ticket: "YOUR_TICKET",
        modelViewUrl: "YOUR_MODEL_VIEW_URL",
        id: "mySession",
      });
    };
    init();

    // Cleanup: close viewport and session when the component unmounts
    return () => {
      session?.close();
      viewport?.close();
    };
  }, []);

  return <canvas ref={canvasRef} style={{ width: "100%", height: "500px" }} />;
};

export default ShapeDiverViewer;

Why Cleanup Matters (React Strict Mode)

React 18 runs useEffect callbacks twice in development when Strict Mode is enabled. Without proper cleanup, you'll see errors like "Viewport with this ID already exists". The cleanup function (the return () => { ... } in the effect) ensures the previous viewport and session are closed before new ones are created.

Avoiding Stale Closures

A common issue in React is that event listeners registered in a useEffect capture the state value at the time the effect ran. If state updates later, the captured value becomes stale. Use useRef to always read the current value:

TYPESCRIPT
import { useRef, useEffect, useState } from "react";
import { addListener, removeListener, EVENTTYPE_INTERACTION } from "@shapediver/viewer";

const [selectedItem, setSelectedItem] = useState<string | null>(null);
const selectedItemRef = useRef(selectedItem);
selectedItemRef.current = selectedItem;

useEffect(() => {
  const token = addListener(EVENTTYPE_INTERACTION.SELECT, (event) => {
    // Read from ref — always has the current value
    console.log("Previously selected:", selectedItemRef.current);
    setSelectedItem(event.node?.name ?? null);
  });

  return () => removeListener(token);
}, []);

Next.js Integration

Server-Side Rendering (SSR)

The ShapeDiver Viewer requires browser APIs (DOM, WebGL) and cannot run during server-side rendering. If you import the viewer in a Next.js component that is rendered on the server, you'll get:

TEXT
ReferenceError: window is not defined

Solution: Use next/dynamic with SSR disabled:

TYPESCRIPT
import dynamic from "next/dynamic";

// Dynamically import the viewer component with SSR disabled
const ShapeDiverViewer = dynamic(
  () => import("../components/ShapeDiverViewer"),
  { ssr: false }
);

export default function Page() {
  return (
    <div>
      <h1>My Configurator</h1>
      <ShapeDiverViewer />
    </div>
  );
}

The component imported by dynamic() should contain the useEffect-based initialization described in the React section above.

App Router (Next.js 13+)

If you're using the Next.js App Router, make sure the component that uses the viewer is a Client Component:

TYPESCRIPT
"use client";

import { useEffect, useRef } from "react";
import { createViewport, createSession } from "@shapediver/viewer";

// ... same pattern as the React example above

General Best Practices

  • Always clean up. Close viewports and sessions when your component unmounts or when navigating away. Failing to do so leaks WebGL contexts and network connections.
  • Use unique IDs. If your application can create multiple viewer instances, ensure each viewport and session has a unique id.
  • Debounce parameter updates. When connecting UI controls (sliders, color pickers) to session parameters, debounce the customize() call to avoid rate limiting. See the Sessions page for examples.
  • Handle canvas sizing. Ensure the canvas container has explicit dimensions. Use a ResizeObserver to call viewport.resize() when the container changes size. See the Viewports page.

For common issues and their solutions, see the Troubleshooting & Common Errors page.

JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.