Chapter 64: Using Kitsune With React (and Other Frameworks)
Kitsune shouldn’t require abandoning React.
Most teams reading this book have a React application. Chapter 70 (the migration story in Part VII) walked through the strangler-fig pattern — using Kit components inside React, the runtime as an enhancement layer, sections migrated separately. This chapter develops the React adapter in detail and covers the equivalent patterns for Vue, Svelte, Solid, and other framework hosts.
The architectural premise: the Kit runtime operates on the DOM. Any framework that produces DOM can host the runtime. The adapters in this chapter are small bridges that make the integration ergonomic for each framework’s specific patterns.
The React Adapter
Section titled “The React Adapter”@kitsune/react provides hooks that expose the runtime to React components:
import { KitShellProvider, useKitRuntime, useKitProvider } from '@kitsune/react'import { USER } from '@kitsune/core'
function App() { return ( <KitShellProvider name="my-app" modules={[/* the application's modules */]} > <ProfileEditor /> </KitShellProvider> )}
function ProfileEditor() { const runtime = useKitRuntime() const user = useKitProvider(USER)
async function handleSave(data: ProfileData) { const result = await runtime.command({ type: 'profile.save', context: { entity: { type: 'profile', id: user?.current()?.id } }, payload: data }) if (!result.ok) console.error(result.error) }
return ( <form onSubmit={(e) => { e.preventDefault(); handleSave({/* ... */}) }}> {/* form content */} </form> )}The KitShellProvider component creates the shell, installs modules, mounts on its child container. The provider component handles the React-specific lifecycle (mount/unmount, effect cleanup) so the shell starts and stops alongside the React tree.
The hooks expose the runtime to descendant components. useKitRuntime returns the active runtime; useKitProvider(TOKEN) injects the named provider. Both hooks subscribe to relevant changes — if a module re-registers a provider, dependent components re-render.
Web Components Inside React
Section titled “Web Components Inside React”The most direct integration is rendering Kit components inside React:
import '@kitsune/components/kit-button'
function MyComponent() { return ( <div data-meta-surface="my-component"> <kit-button variant="primary" meta-event="my.action" > Click me </kit-button> </div> )}The custom element renders. The runtime (if installed via the shell provider) observes the click. The metadata boundary collects context. The event flows through the architecture.
For React 19 and later, the integration is seamless — React handles property-vs-attribute distinction correctly for known properties, automatically converts camelCase to kebab-case, and forwards event listeners properly. For earlier React versions, the patterns from Chapter 70 (using ref for object properties, addEventListener for custom events) apply.
The @lit/react package provides typed React wrappers for Lit elements. Kit ships these wrappers as part of @kitsune/react/components:
import { KitButton } from '@kitsune/react/components'
function MyComponent() { return ( <KitButton variant="primary" onClick={(e) => console.log('clicked')} onKitChange={(detail) => console.log('changed', detail)} > Click me </KitButton> )}The wrapped component has fully typed props, proper event-prop conversion (camelCase onKitChange becomes the kebab-case kit-change event listener), and TypeScript autocomplete. The underlying element is still a real <kit-button>; the wrapper is ergonomic only.
The Vue Adapter
Section titled “The Vue Adapter”Vue’s pattern is similar:
<template> <KitShellProvider name="my-app" :modules="modules"> <ProfileEditor /> </KitShellProvider></template>
<script setup>import { KitShellProvider, useKitRuntime } from '@kitsune/vue'import ProfileEditor from './ProfileEditor.vue'
const modules = [/* the application's modules */]</script><template> <form @submit.prevent="handleSave"> <kit-text-field v-model="displayName"></kit-text-field> <kit-button type="submit">Save</kit-button> </form></template>
<script setup>import { ref } from 'vue'import { useKitRuntime } from '@kitsune/vue'
const runtime = useKitRuntime()const displayName = ref('')
async function handleSave() { await runtime.command({ type: 'profile.save', payload: { displayName: displayName.value } })}</script>The pattern is the same as React’s. Vue’s compositional API integrates with the runtime through useKitRuntime and the equivalent injection helpers. Kit components are real custom elements; Vue’s template compiler treats them correctly.
The Svelte Adapter
Section titled “The Svelte Adapter”Svelte’s reactivity model integrates naturally:
<script> import { getKitRuntime, getKitProvider } from '@kitsune/svelte' import { USER } from '@kitsune/core'
const runtime = getKitRuntime() const user = $derived(getKitProvider(USER)?.current())
let displayName = $state('')
async function handleSave() { await runtime.command({ type: 'profile.save', payload: { displayName } }) }</script>
<form on:submit|preventDefault={handleSave}> <kit-text-field bind:value={displayName} /> <kit-button type="submit">Save</kit-button></form>The runtime is exposed through Svelte’s context API. The $state and $derived runes (Svelte 5) compose with the runtime’s events through a small adapter that re-emits runtime events as Svelte stores when needed.
The Solid Adapter
Section titled “The Solid Adapter”Solid’s fine-grained reactivity has a similar integration:
import { KitShellProvider, useKitRuntime } from '@kitsune/solid'
function ProfileEditor() { const runtime = useKitRuntime() const [displayName, setDisplayName] = createSignal('')
async function handleSave(e: SubmitEvent) { e.preventDefault() await runtime.command({ type: 'profile.save', payload: { displayName: displayName() } }) }
return ( <form onSubmit={handleSave}> <kit-text-field value={displayName()} onChange={(e) => setDisplayName(e.target.value)} /> <kit-button type="submit">Save</kit-button> </form> )}Solid’s JSX renders Kit components as real custom elements. The signals integrate with the runtime through the same hook pattern.
Bidirectional Integration
Section titled “Bidirectional Integration”The adapters don’t only let React (or Vue, or Svelte, or Solid) consume the runtime. They also let Kit components consume framework-provided data.
A common pattern: the framework owns the application’s main state; the runtime handles the cross-cutting concerns. A React component can dispatch events into the runtime; modules subscribed to those events can call back into the framework if needed:
function App() { const [todos, setTodos] = useState<Todo[]>([])
// Provide the React state through the runtime's provider system // so modules can read it (carefully — modules shouldn't mutate React state directly) return ( <KitShellProvider name="todo-app" modules={[/* ... */]} providers={[ { token: TODOS_PROVIDER, value: () => todos } ]} > <TodoList todos={todos} onAdd={(t) => setTodos([...todos, t])} /> </KitShellProvider> )}The pattern is small. The application’s primary state stays in React. The runtime’s modules can read it through the provider. The runtime doesn’t try to replace React’s state; it augments it with capability-layer concerns.
When the Adapter Is the Right Choice
Section titled “When the Adapter Is the Right Choice”The adapter is the right choice when:
The application is mostly React (or Vue, etc.) and the team wants to keep most of it. The adapter lets specific capabilities (analytics, audit, observability) migrate to the Kit architecture without rewriting the rest.
The team wants progressive adoption. Start with the adapter, add the runtime, install modules, leave the existing components mostly unchanged. Over time, individual components can be replaced with Kit equivalents.
Multiple framework choices coexist. A team using React for the main application and a Vue widget elsewhere can use a single shared runtime through both adapters. The architecture is framework-agnostic; the adapters are language-specific veneers.
Existing React-based libraries are in use. A team depending on Material UI, Chakra UI, or a custom design system can keep using them while adopting Kit’s capability modules. The visual layer doesn’t have to change for the architecture to deliver value.
When the Adapter Isn’t Needed
Section titled “When the Adapter Isn’t Needed”The adapter isn’t needed when:
The application is being built fresh. Going Kit-native from the start is simpler than going through the adapter layer.
The team has fully migrated. Once the last React component is replaced, the adapter isn’t doing anything.
The migration target is a specific renderer that isn’t React/Vue/Svelte/Solid. Server-rendered HTML, Astro pages, AI-generated markup, etc. — these don’t need a framework adapter; the runtime attaches directly to the DOM.
Bridge to What Kitsune Is Not
Section titled “Bridge to What Kitsune Is Not”The final chapter of Part VI (Chapter 65) takes the honest position on what the architecture doesn’t try to be. The framework comparisons. The places where other tools are the better answer. The closing acknowledgments of what’s outside scope.
Exercise: Migrate One Component
Section titled “Exercise: Migrate One Component”Take an existing React application. Pick one component — a button, a form field, a small widget — and replace its implementation with a Kit equivalent using the React adapter.
The migration:
- Install
@kitsune/reactand@kitsune/components. - Wrap the application in
<KitShellProvider>with at least the analytics and audit modules. - Replace the chosen component’s implementation. Where it previously imported analytics, replace with a Kit event declaration.
- Run the application. Verify the component still works visually and functionally.
- Inspect the diagnostic overlay. Verify the events flow through the runtime.
Then think about the next migration target:
- Which component would you migrate next?
- What dependencies would have to come along?
- How much of the application would have to change?
The exercise is the migration story made concrete. Most teams adopting Kit will start with a single component, see the architecture’s behavior in their own codebase, and decide based on real evidence whether to keep migrating. The adapter makes the first step low-stakes.