React
Kitsune’s runtime is framework-neutral. The @atheory-ai/kitsune-react adapter lets a React app participate in the same loop as a Lit/web-components app — same boundaries, same events, same commands, same modules.
You can adopt Kitsune in a React codebase without rewriting components. Drop a <KitBoundary> around a feature, add data-meta-event to an existing button, and you have analytics on that button without changing its props.
Install
Section titled “Install”pnpm add @atheory-ai/kitsune-core @atheory-ai/kitsune-react react react-domIf you also want the UI elements (<kit-button>, <kit-dialog>, etc.):
pnpm add @atheory-ai/kitsune-app @atheory-ai/kitsune-uiThen in your entry:
import '@atheory-ai/kitsune-app'import '@atheory-ai/kitsune-ui'This registers the custom elements globally so they upgrade wherever React renders them.
The provider
Section titled “The provider”Wrap your app’s root with <KitShellProvider>:
import { KitShellProvider } from '@atheory-ai/kitsune-react'import { notificationModule, dialogModule } from '@atheory-ai/kitsune-ui'import { debugModule } from '@atheory-ai/kitsune-dev'
const modules = [notificationModule(), dialogModule(), debugModule()]
export function App() { return ( <KitShellProvider modules={modules}> <Main /> </KitShellProvider> )}KitShellProvider creates a runtime, installs the modules on mount, starts it, and tears down on unmount. Pass an existing runtime if you want to construct it yourself.
Boundaries
Section titled “Boundaries”Use <KitBoundary> exactly the way you’d use <kit-boundary>:
import { KitBoundary } from '@atheory-ai/kitsune-react'
export function CampaignPage({ campaignId }) { return ( <KitBoundary surface="campaign-page" feature="donations"> <Hero /> <KitBoundary surface="donation-form" entityType="campaign" entityId={campaignId} > <DonationForm /> </KitBoundary> </KitBoundary> )}Children of a boundary — React components, plain JSX elements, or web components — can emit events through data-meta-* attributes:
function DonateButton() { return ( <button data-meta-event="donation.started" data-meta-command="dialog.open" data-meta-prop-target="checkout-dialog" > Donate </button> )}The boundary’s click delegate parses these and emits through the runtime, with full context attached. Just like the Lit version.
For programmatic dispatch:
import { useKitEmit, useKitCommand } from '@atheory-ai/kitsune-react'
function SignupForm() { const emit = useKitEmit() const command = useKitCommand()
return ( <form onSubmit={async (e) => { e.preventDefault() const data = Object.fromEntries(new FormData(e.currentTarget))
const result = await command({ type: 'session.signup', payload: data, })
if (result.ok) { emit({ type: 'session.signup_succeeded', payload: { userId: result.value.id } }) } }}> ... </form> )}Other hooks:
useKitRuntime()— the raw runtime, forruntime.on(),runtime.inject(), etc.useKitContext()— the current boundary’s context, useful for displaying surface info or for child boundaries that want to read inherited values.
Web components inside React
Section titled “Web components inside React”Custom elements work in React without the adapter. React 19 has full support for custom-element props and event listeners. Earlier versions need string-only attributes — which is what Kitsune’s metadata protocol uses anyway.
function SaveButton() { return ( <kit-button meta-event="settings.saved" meta-command="notification.show" meta-prop-message="Saved" > Save </kit-button> )}Note: TypeScript may need a global JSX declaration for kit-button. The adapter exports nothing for this — declare it once in your project:
import type { KitButtonElement } from '@atheory-ai/kitsune-ui'
declare module 'react' { namespace JSX { interface IntrinsicElements { 'kit-button': React.DetailedHTMLProps< React.HTMLAttributes<KitButtonElement>, KitButtonElement > & { disabled?: boolean } } }}A real adoption story
Section titled “A real adoption story”Here’s the smallest possible “I have an existing React app and I want analytics on the checkout button” path:
// 1. Add the provider at the rootimport { KitShellProvider } from '@atheory-ai/kitsune-react'
const analyticsStub = { name: 'analytics', events: { '*': (e) => console.log('[analytics]', e.type, e.context, e.payload) },}
<KitShellProvider modules={[analyticsStub]}> <App /></KitShellProvider>
// 2. Wrap the checkout feature in a boundary<KitBoundary surface="checkout" feature="commerce" entityType="cart" entityId={cart.id}> <CheckoutPage /></KitBoundary>
// 3. Add metadata to the button (no other change to the component)<button className="primary-button" onClick={onCheckout} data-meta-event="checkout.started"> Pay now</button>You now have analytics on that button. Surface, feature, entity, and timestamp all attached. Zero React state changes. Zero other components touched.
What’s not in the adapter
Section titled “What’s not in the adapter”The React adapter is intentionally small. It does not provide:
- A React-flavored Lit component renderer (use Lit’s React wrapper if you want one).
- Server components support — Kitsune runs in the browser; if you’re using RSC, render the boundary on the client.
- A React-specific module API — modules are framework-neutral by design.
What’s next
Section titled “What’s next”- Adopting Incrementally — a longer playbook for migrating an existing app.
- Other Frameworks — the protocol works without any adapter.
- Reference: kit-boundary