Host Octane inside React
React compatibility
Already have a React 19 app? octane/react lets you render compiled Octane islands inside it — one component at a time, no rewrite. Events stay native, context is shared, and it renders on the server.
What OctaneCompat is
octane/react exports a single component, <OctaneCompat>, that hosts a compiled
Octane subtree inside a real React 19 tree. React owns the wrapper and one host
element; a private Octane root owns every descendant — the island.
Because Octane compiles ahead of time, the island is already the framework's own
render output. <OctaneCompat> just gives it a place to live inside React, so you can
port a screen, a widget, or a single component without touching the React code around
it.
Local Octane @try / Suspense / error boundaries inside the island always get first
chance to handle a throw or a pending state; only something that escapes the island's
own root surfaces to the nearest React Suspense or error boundary.
Set up the toolchain
An Octane island is a compiled .tsrx (or Octane-compiled .tsx) module, while the
host app is compiled by React's own toolchain. requireDirective: true tells the two
compilers apart: every .tsrx is Octane's by extension, and an Octane-owned
.tsx/.ts/.js opts in with a leading /** @jsxImportSource octane */ pragma:
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { octane } from '@octanejs/vite-plugin';
export default defineConfig({
plugins: [octane({ requireDirective: true }), react()],
});// src/islands/Counter.tsrx — compiled by Octane, no marker needed
import { useState } from 'octane';
export function Counter(props: { start: number }) @{
const [count, setCount] = useState(props.start);
<button onClick={() => setCount(count + 1)}>{'clicks: ' + count}</button>
}The ownership rules — what the pragma means and how exclude hands a path to
another toolchain — live in
Build tools. This page is about what you do once
the compilers are wired up.
Render an island
Import the compiled Octane component and the host, then wrap the component in
<OctaneCompat>. It takes exactly one compiled Octane element as its child, consumed
as a { component, props } transport — React never invokes it:
// App.tsx — compiled by React's toolchain
import { OctaneCompat } from 'octane/react';
import { Counter } from './islands/Counter.tsrx';
export function App() {
return (
<main>
<h1>My React app</h1>
<OctaneCompat>
<Counter start={3} />
</OctaneCompat>
</main>
);
}Both the child site and the island are fully typed: under octane's TypeScript tooling
(tsrx-tsc, the editor plugin) the .tsrx export keeps its own octane-typed
signature, React JSX accepts it zero-cast, and a wrong, missing, or unknown island
prop is a type error at <Counter …/>. (Octane element values remain rejected in
ordinary React ReactNode positions — only the component transport crosses the
boundary.)
The equivalent component/props form passes the same transport explicitly, with
props checked against the island's props parameter; island identity follows the
React key on <OctaneCompat> itself, and props may be omitted for an island that
takes none:
<OctaneCompat component={Counter} props={{ start: 3 }} />Events inside the island are native and delegated at the island host, so React
ancestors observe real capture and bubble order, the real target, and
stopPropagation() / preventDefault() exactly as the platform reports them — there
is no synthetic event layer bridging the two.
Share React context
An island reads your app's real React contexts with ordinary use() / useContext() —
pass the React context object straight through:
// src/islands/ThemedBadge.tsrx
import { use } from 'octane';
import { ThemeContext } from '../theme.ts';
export function ThemedBadge() @{
const theme = use(ThemeContext);
<span className={'badge badge-' + theme}>Octane island</span>
}The host resolves the nearest committed provider value and keeps the island in sync as
the provider updates — no adapter or prop-drilling required. Reading a context that has
no provider yields its createContext default, just as it does in React.
Server rendering and hydration
Islands render on the server too. Wherever React runs on the server — Fizz streaming or
renderToString — import the host from octane/react/server; the client entry
(octane/react) hydrates its output.
// entry.server.tsx
import { OctaneCompat } from 'octane/react/server';Each island runs one synchronous server pass; an unresolved island suspension is
delegated to React so the shell can stream and fill in. Scoped island CSS is emitted as
React 19 style resources (a stable per-hash href with precedence="octane"), so Fizz
hoists and deduplicates it across islands. The island's HTML is written as an opaque
region the client adopts without re-rendering, so hydration never touches the
descendants React did not own.
What isn't supported
One part of React does not cross over:
- React Server Components — RSC, Flight, and
cache(). Islands are client and server-rendered function components; there is no Server Component boundary between React and Octane.
Everything else you rely on in a React app comes along: hooks, Suspense, transitions, context, portals, error boundaries, native events, and server rendering with hydration. See Differences from React for the full list of APIs Octane leaves out.
Editor and type checking
Islands keep their real octane types across the boundary — no declare module '*.tsrx' shims. Two pieces of setup make that work in a React host project:
- Type checking: use
tsrx-tscinstead of plaintscso.tsrxislands join the program with their exported types:
{ "scripts": { "typecheck": "tsrx-tsc --noEmit -p tsconfig.json" } }- Editor: register the TypeScript plugin in
tsconfig.jsonso imports like./islands/PriceBadge.tsrxresolve with full types in.tsxfiles (requires the editor to use TypeScript 5.9/6.x — the plugin API is not available under TS 7 previews):
{ "compilerOptions": { "plugins": [{ "name": "@tsrx/typescript-plugin" }] } }The host tsconfig stays pure React ("jsx": "react-jsx", no
jsxImportSource) — each .tsrx island types against octane's JSX on its
own, and prop mistakes at <OctaneCompat component={Island} props={…} /> or
<Island /> call sites surface as ordinary type errors. See
examples/harbor for a complete working setup.
Next
- Build tools — mixed-toolchain ownership:
.tsrxby extension, octane.tsx/.ts/.jsvia the@jsxImportSource octanepragma. - Quick start — install Octane and write your first
.tsrxcomponent. - Differences from React — the deliberate divergences (everything else matching React is the point).