Wed Jan 10 2024
software

Adding Interactive Code Demos to My Next.js Blog with Sandpack

Jeremy Atkinson
Developer

I wanted to include live, editable code examples in my blog posts without the hassle of maintaining separate CodeSandbox links. Sandpack solved this perfectly by bringing the CodeSandbox experience directly into my MDX posts.

The Setup

First, I installed Sandpack from npm. Information about Sandpack is available here

I created a reusable Sandbox component that wraps Sandpack with sensible defaults for my R3F demos:

// components/r3f/Sandbox.jsx
import { Sandpack } from "@codesandbox/sandpack-react";

export default async function Sandbox({ filePath, ...props }) {
  let defaults = {
    template: "react",
    theme: "dark",
    options: {
      editorHeight: 500
    },
    customSetup: {
      dependencies: {
        "three": "^0.178.0",
        "@react-three/fiber": "^9.3.0",
        "@react-three/drei": "^10.6.1",
        "leva": "^0.10.0",
      },
    }
  };
  
  const config = { ...defaults, ...props }
  // File loading logic here...
  
  return <Sandpack {...config} />
}

File Loading Feature

One cool feature I added was automatic file loading to avoid having to duplicate code. Instead of copying code into markdown files, I can reference external files and run them in the sandbox:

export function getComponentCode(componentPath) {
  const fullPath = path.join(process.cwd(), componentPath);
  return fs.readFileSync(fullPath, 'utf8');
}

If a filePath prop is provided, the component loads the file and injects it as /App.js in the sandbox. This keeps my blog posts clean and lets me maintain the actual demo code separately.

MDX Integration

To use the component in MDX, I registered it in mdx-components.tsx:

import Sandbox from "./components/r3f/Sandbox";

export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    Sandbox: Sandbox,
    ...components,
  };
}

Now I can drop interactive demos into any blog post:

<Sandbox filePath="demos/some-demo.jsx" />

Why I Like this

Clean separation: Demo code lives in separate files, blog posts stay readable.

Consistent setup: All my R3F demos get the same dependencies and configuration automatically.

Flexible override: I can still pass custom files or configurations when needed.

Zero maintenance: No external links to break or sandboxes to update separately.

Here is an example of the component in action:

'use client'
import { useRef } from 'react'
import { useFrame, Canvas } from '@react-three/fiber'
import { OrbitControls, MeshWobbleMaterial } from '@react-three/drei'

export function Cube() {
  const meshRef = useRef()
  useFrame(() => {
    if (meshRef.current) {
      meshRef.current.rotation.x += 0.01
      meshRef.current.rotation.y += 0.01
    }
  })
  return (
    <mesh ref={meshRef}>
      <boxGeometry args={[1, 1, 1]} />
      <MeshWobbleMaterial factor={0.6} speed={1} color="orange" />
    </mesh>
  )
}

export default function App() {
  return (
    <Canvas style={{ height: '90vh' }}>
      <ambientLight intensity={0.5} />
      <pointLight position={[10, 10, 10]} />
      <Cube />
      <OrbitControls />
    </Canvas>
  )
}

Jeremy Atkinson

Jeremy is a structural engineer, researcher, and developer from BC. He works on Calcs.app and writes at Kinson.io

Comments (0)

Loading comments...