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:
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:
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:
ReferenceError: window is not defined
Solution: Use next/dynamic with SSR disabled:
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:
"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
ResizeObserverto callviewport.resize()when the container changes size. See the Viewports page.
For common issues and their solutions, see the Troubleshooting & Common Errors page.