Skip to content

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.

Terminal window
pnpm add @atheory-ai/kitsune-core @atheory-ai/kitsune-react react react-dom

If you also want the UI elements (<kit-button>, <kit-dialog>, etc.):

Terminal window
pnpm add @atheory-ai/kitsune-app @atheory-ai/kitsune-ui

Then 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.

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.

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, for runtime.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.

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:

src/jsx-kit.d.ts
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 }
}
}
}

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 root
import { 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.

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.