Answer

## Answer Three separate fixes, one for each issue. ### 1. Fix SSR crash — use `next/dynamic` with `ssr: false` `'use client'` alone is not enough — Next.js still statically analyzes client component imports at build time and Three.js crashes during that pass. You must lazy-load the component entirely: ```tsx // GraphViz.tsx — the actual component 'use client' import ForceGraph3D from 'react-force-graph-3d' // ... component implementation // GraphVizWrapper.tsx — or inline in your page import dynamic from 'next/dynamic' const GraphViz = dynamic(() => import('./GraphViz').then(m => m.GraphViz), { ssr: false }) ``` The `ssr: false` option prevents the module from even being imported during server render. ### 2. Fix TypeScript errors — cast to `any` at the prop boundary The `react-force-graph-3d` types don't accept generic node shapes well. The cleanest fix without fighting the type system: ```tsx // eslint-disable-next-line @typescript-eslint/no-explicit-any const graphRef = useRef(null) NODE_COLORS[node.type] ?? '#888'} nodeVal={(node: any) => normalizePageRank(node.pageRank)} getLinkColor={(link: any) => EDGE_COLORS[link.type] ?? '#333'} getNodeLabel={(node: any) => `${node.type}: ${node.label.slice(0, 60)}`} nodeThreeObject={(node: any) => buildNodeMesh(node)} /> ``` ### 3. Fix missing `three` module — add it as an explicit dependency `three` is only a transitive dependency of `react-force-graph-3d`. TypeScript and the bundler may not resolve it: ```bash pnpm add three @types/three --filter @your/web-app ``` Then import normally: ```tsx import * as THREE from 'three' ``` ### Custom nodeThreeObject with landmark aura For visually distinct landmark nodes using pageRank-based sizing: ```tsx function buildNodeMesh(node: any): THREE.Object3D { const group = new THREE.Group() const r = normalizePageRank(node.pageRank) // e.g. range [2, 14] const color = node.isStale ? 0x444444 : hexToInt(NODE_COLORS[node.type] ?? '#888888') const opacity = node.isStale ? 0.35 : 1.0 // Core sphere const core = new THREE.Mesh( new THREE.SphereGeometry(r, 16, 16), new THREE.MeshLambertMaterial({ color, transparent: opacity < 1, opacity }) ) if (!node.isStale) { (core.material as THREE.MeshLambertMaterial).emissive = new THREE.Color(color) ;(core.material as THREE.MeshLambertMaterial).emissiveIntensity = 0.45 } group.add(core) // Landmark aura shell if (node.isLandmark) { const aura = new THREE.Mesh( new THREE.SphereGeometry(r * 2.2, 16, 16), new THREE.MeshLambertMaterial({ color, transparent: true, opacity: 0.1, side: THREE.BackSide, }) ) group.add(aura) } return group } ``` **Why `THREE.BackSide` for the aura?** Renders the inside face of the outer sphere, giving a soft glow that surrounds the core without z-fighting.

b59515b0-e1f5-4c17-bdb0-82e0521856a1

Answer

Three separate fixes, one for each issue.

1. Fix SSR crash — use next/dynamic with ssr: false

'use client' alone is not enough — Next.js still statically analyzes client component imports at build time and Three.js crashes during that pass. You must lazy-load the component entirely:

// GraphViz.tsx — the actual component
'use client'
import ForceGraph3D from 'react-force-graph-3d'
// ... component implementation

// GraphVizWrapper.tsx — or inline in your page
import dynamic from 'next/dynamic'
const GraphViz = dynamic(() => import('./GraphViz').then(m => m.GraphViz), { ssr: false })

The ssr: false option prevents the module from even being imported during server render.

2. Fix TypeScript errors — cast to any at the prop boundary

The react-force-graph-3d types don't accept generic node shapes well. The cleanest fix without fighting the type system:

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const graphRef = useRef(null)

 NODE_COLORS[node.type] ?? '#888'}
  nodeVal={(node: any) => normalizePageRank(node.pageRank)}
  getLinkColor={(link: any) => EDGE_COLORS[link.type] ?? '#333'}
  getNodeLabel={(node: any) => `${node.type}: ${node.label.slice(0, 60)}`}
  nodeThreeObject={(node: any) => buildNodeMesh(node)}
/>

3. Fix missing three module — add it as an explicit dependency

three is only a transitive dependency of react-force-graph-3d. TypeScript and the bundler may not resolve it:

pnpm add three @types/three --filter @your/web-app

Then import normally:

import * as THREE from 'three'

Custom nodeThreeObject with landmark aura

For visually distinct landmark nodes using pageRank-based sizing:

function buildNodeMesh(node: any): THREE.Object3D {
  const group = new THREE.Group()
  const r = normalizePageRank(node.pageRank) // e.g. range [2, 14]
  const color = node.isStale ? 0x444444 : hexToInt(NODE_COLORS[node.type] ?? '#888888')
  const opacity = node.isStale ? 0.35 : 1.0

  // Core sphere
  const core = new THREE.Mesh(
    new THREE.SphereGeometry(r, 16, 16),
    new THREE.MeshLambertMaterial({ color, transparent: opacity < 1, opacity })
  )
  if (!node.isStale) {
    (core.material as THREE.MeshLambertMaterial).emissive = new THREE.Color(color)
    ;(core.material as THREE.MeshLambertMaterial).emissiveIntensity = 0.45
  }
  group.add(core)

  // Landmark aura shell
  if (node.isLandmark) {
    const aura = new THREE.Mesh(
      new THREE.SphereGeometry(r * 2.2, 16, 16),
      new THREE.MeshLambertMaterial({
        color,
        transparent: true,
        opacity: 0.1,
        side: THREE.BackSide,
      })
    )
    group.add(aura)
  }

  return group
}

Why THREE.BackSide for the aura? Renders the inside face of the outer sphere, giving a soft glow that surrounds the core without z-fighting.