Develop components in isolation
Storybook
Render native Octane components written in TSRX inside a React + Vite Storybook. Keep ordinary CSF3 stories and use OctaneCompat to host each component.
Run the example
The Storybook example
contains a native Counter.tsrx, a shared asReact helper, and typed stories.
From the Octane repository root, using its Node.js and pnpm versions:
pnpm install
pnpm --filter storybook-example devOpen the local URL printed by Storybook and select Octane → Counter. Change
label, step, or disabled in Controls after incrementing the counter: its
local state stays intact. Switch to Large Step and back to start a fresh
counter.
Configure Vite
Use Storybook's React + Vite framework with matching React and React DOM 19 versions. The example's package manifest and workspace lockfile record the tested dependencies.
Configure the component workbench with Octane's compiler plugin:
// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';
import react from '@vitejs/plugin-react';
import { octane } from 'octane/compiler/vite';
import { mergeConfig } from 'vite';
const config: StorybookConfig = {
stories: ['../src/**/*.stories.tsx'],
framework: '@storybook/react-vite',
typescript: { reactDocgen: false },
async viteFinal(config) {
return mergeConfig(config, {
plugins: [octane({ requireDirective: true }), react()],
optimizeDeps: { exclude: ['octane'], include: ['octane > devalue'] },
});
},
};
export default config;Keep Octane before the single React plugin. If your consumer Vite config already
supplies these plugins, keep them there instead of adding a second instance in
viteFinal. This workbench uses octane/compiler/vite; it does not need the
full application integration from @octanejs/vite-plugin.
Octane compiles .tsrx components. With requireDirective: true, unmarked
application TSX stays React-owned; the leading /** @jsxImportSource react */
pragma also selects React's JSX types for stories and the helper below. See
compiler ownership for mixed projects.
The optimizer settings support the example's linked Octane source package.
Excluding octane keeps that source in the compiler pipeline; eagerly including
its devalue dependency avoids a late optimization reload on the first preview.
Share the bridge
Create one small helper for the stories in your project:
/** @jsxImportSource react */
// src/asReact.tsx
import { OctaneCompat, type OctaneHostedComponent } from 'octane/react';
export function asReact<P>(component: OctaneHostedComponent<P>) {
return function OctaneWrapper(props: P) {
return <OctaneCompat component={component} props={props} />;
};
}OctaneCompat lets React host the component while Octane owns its hooks, native
events, state, and DOM. The helper infers the native component's props, so each
story needs only a one-line alias. Create that alias once at module scope to
keep the React component identity stable across Controls updates.
Write typed stories
Import your native component into an ordinary .stories.tsx file. This example
uses the counter
with label, step, and disabled props:
/** @jsxImportSource react */
// src/Counter.stories.tsx
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Counter } from './Counter.tsrx';
import { asReact } from './asReact.tsx';
const CounterStory = asReact(Counter);
const meta = {
title: 'Octane/Counter',
component: CounterStory,
args: { label: 'Increment', step: 1, disabled: false },
argTypes: {
label: { control: 'text' },
step: { control: 'number' },
disabled: { control: 'boolean' },
},
} satisfies Meta<typeof CounterStory>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const LargeStep: Story = {
args: { label: 'Add five', step: 5 },
};Set component to the React wrapper so React invokes the bridge. Passing the
raw Octane function would give React a component owned by another renderer.
Explicit args and argTypes supply Controls; automatic native .tsrx prop
documentation is not part of this recipe, so React docgen is disabled.
Check and build
Typecheck programs containing .tsrx with octane-tsc. The example's
TypeScript configuration
keeps React JSX typing and native Octane prop checking without casts or ambient
declare module '*.tsrx' shims.
pnpm --filter storybook-example typecheck
pnpm --filter storybook-example buildThe static site is written to examples/storybook/dist. To run the example's
browser checks against both development Storybook and the served static build:
pnpm --filter storybook-example exec playwright install chromium
pnpm --filter storybook-example test:e2eLimits
This is a client-rendered component workbench. OctaneCompat adds a div host;
place it where a div is valid, rather than directly inside a table row, select,
or SVG tree. Component and style edits update the preview; preserving local
state through Fast Refresh is not guaranteed.
The stories stay in .stories.tsx; .stories.tsrx discovery and Storybook
repository changes are not needed. The helper adapts Octane components only.
Other TSRX targets, such as Solid and Vue, need their own compiler and compatible
Storybook renderer. See React compatibility for the bridge's
full behavior and boundaries.