Create a new site

npm init capri my-capri-site -- -e react

This will download and install capri-js/capri/examples/react.

You can view a deployed version of the demo on GitHub pages, including the preview SPA.

Entry files

The client entry file is a regular React single page app. We render a <PreviewBanner> on top of the app, so users know that they are viewing the SPA version, which is usually the case when they are editing the site in a CMS.

// src/main.tsx

import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";

import { App } from "./App";
import { PreviewBanner } from "./Preview.jsx";

    <PreviewBanner />
    <App />

On the server, we use renderToString and a StaticRouter instead:

// src/main.server.tsx

import { renderToString } from "@capri-js/react/server";
import { StaticRouter } from "react-router-dom/server.js";

import { App } from "./App";

export async function render(url: string) {
  return {
    "#app": await renderToString(
      <StaticRouter location={url}>
        <App />


you can define interactive islands by naming your components *.island.tsx:

// src/Counter.island.tsx

import { useState } from "react";

export default function Counter() {
  const [counter, setCounter] = useState(start);
  return (
      <button onClick={() => setCounter((c) => c - 1)}>-</button>
      <button onClick={() => setCounter((c) => c + 1)}>+</button>

Media queries

You can export an options object to hydrate an island as soon as a media query matches. The following example will hydrate once the viewport width gets below 700px:

import { useEffect, useState } from "react";

export const options = {
  media: "(max-width:700px)",

export default function MediaQuery() {
  const [content, setContent] = useState(
    "Resize your browser below 700px to hydrate this island."
  useEffect(() => {
    setContent("The island has been hydrated.");
  }, []);
  return <div>{content}</div>;

Data fetching

You can either let your components fetch data or use a router that supports data fetching.

Here is an example that uses TanStack Router to load some data.

Another option is to use SWR version 1.x:

// src/Profile.tsx

const fetcher = (...args) => fetch(...args).then((res) => res.json());

export function Profile() {
  const user = useSWR("", fetcher, { suspense: true });
  return <div>Hello {}!</div>;

Add a <Suspense> boundary somewhere above in your component tree where you want to render a fallback while the data is loading. The fallback will only be shown in the preview SPA. When generating static pages, Capri will wait until all data is loaded.

// src/App.tsx

export function App() {
  return (
    <Suspense fallback={<div>loading...</div>}>
      <Profile />

This will only work with SWR 1.x due to this change. It's not entirely clear if/how data fetching inside a component is supposed to work in the future, so letting the router fetch data outside of React is currently the best option.

MIT Licensed | Copyright © 2022 | Impressum