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.
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";
ReactDOM.createRoot(document.getElementById("app")!).render(
<BrowserRouter>
<PreviewBanner />
<App />
</BrowserRouter>
);
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 />
</StaticRouter>
),
};
}
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 (
<div>
<button onClick={() => setCounter((c) => c - 1)}>-</button>
<span>{counter}</span>
<button onClick={() => setCounter((c) => c + 1)}>+</button>
</div>
);
}
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>;
}
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("https://api.example.com", fetcher, { suspense: true });
return <div>Hello {user.name}!</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 />
</Suspense>
);
}
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.