React components in a static site

Sometimes you want a React component inside an otherwise-static page โ€” an interactive counter, a clock, a filterable table โ€” without turning the whole site into an SPA. That's what reactor is for: render the component to HTML at build time, inject it into a template, and optionally hydrate it on the client.

The component

A plain component with a default export:

// src/js/Counter.jsx
import { useState } from 'react'

export default function Counter() {
  const [n, setN] = useState(0)
  return (
    <button onClick={() => setN(n + 1)}>
      Clicked {n} times
    </button>
  )
}

The hydration entry

A tiny client entry that hydrates the pre-rendered markup:

// src/js/counter-hydrate.jsx
import { hydrateRoot } from 'react-dom/client'
import Counter from './Counter.jsx'

hydrateRoot(document.getElementById('counter'), <Counter />)

The config

{
  "reactor": [
    {
      "component": "src/js/Counter.jsx",
      "inject": "counter_html",
      "in": "src/js/counter-hydrate.jsx",
      "out": "dist/js/counter.js",
      "options": { "minify": true, "target": "es2019" }
    }
  ]
}

Poops renders Counter with renderToString and stores the HTML under the inject name. The client entry is bundled to out.

The template

Drop the rendered HTML in, then load the hydration bundle:

<h1>Dashboard</h1>
<div id="counter">{{ counter_html | safe }}</div>
<script src="{{ relativePathPrefix }}js/counter.min.js"></script>

The page ships as real HTML โ€” the button is visible before any JS runs โ€” and becomes interactive once React hydrates it.

Note

React comes from your project's node_modules (npm i react react-dom), and reactor is its own pipeline, independent from scripts โ€” details in React.

Server-only components

Need the HTML but no interactivity (an icon, a formatted block)? Omit in/out and skip the hydration bundle entirely:

{
  "reactor": [
    { "component": "src/js/Icon.jsx", "inject": "icon_html" }
  ]
}

Tip

Multiple components? Add multiple reactor entries, each with its own inject name, and use each in the templates that need it.

Next: A complete React static site.