React / TypeScript Implementation โ
The React implementation splits cleanly into two layers: a framework-agnostic TypeScript host, and a thin React adapter of four hooks. The host runs equally in Vue, Svelte, or Node. The React layer is the only file with a framework dependency.
rune-host.ts โ Framework-Agnostic Host โ
The same four stores as the C# implementation, in TypeScript. No React import. No framework coupling.
export class RuneHost {
readonly state = new RuneStateStore();
readonly actions = new RuneActionRegistry();
readonly intent = new RuneIntentStore();
read(identifier: string): unknown // @ โ resolves dot-notation
sync(identifier: string, value: unknown) // ~ โ write to mutable state
act(actionName: string, ...args): Promise<void> // ! โ dispatch action
recordIntent(path: string, annotation: string) // ? โ record annotation
subscribe(fn: Subscriber): () => void // for React re-renders
}Build the host once, outside React โ not per-render:
function buildTaskHost(): RuneHost {
const host = new RuneHost();
host.state.declare('tasks', [] as TaskItem[]);
host.state.declare('new-task', '');
host.state.declareComputed('pending',
() => (host.read('tasks') as TaskItem[]).filter(t => !t.done));
host.recordIntent('new-task', 'cleared after add-task fires');
host.actions.register('add-task', () => {
const title = (host.read('new-task') as string).trim();
if (!title) return;
host.sync('tasks', [...(host.read('tasks') as TaskItem[]),
{ id: crypto.randomUUID(), title, done: false }]);
host.sync('new-task', '');
});
return host;
}rune-react.tsx โ The Four Hooks โ
The entire React layer. One provider, four hooks, one optional component.
// Provider โ one per screen
<RuneProvider host={host}>
{children}
</RuneProvider>
// @ read โ subscribes to state, re-renders on change
const pending = useRead<TaskItem[]>('pending');
// ~ sync โ returns [value, setter] โ controlled input in one call
const [newTask, setNewTask] = useSync<string>('new-task');
// ! act โ returns stable dispatch function
const addTask = useAct('add-task');
// ? intent โ registers annotation once on mount, renders nothing
useIntent('screen', 'mobile task list, focus on speed');
// Or as a component
<RuneIntent path="screen" annotation="mobile task list, focus on speed" />useRead subscribes to state changes and triggers re-renders when the root key changes. useSync composes useRead with a stable setter. useAct returns a memoised dispatch. useIntent fires once on mount โ no runtime effect, no re-render.
example.tsx โ Usage Patterns โ
Pattern 1 โ Hook-based (idiomatic React)
const taskHost = buildTaskHost();
export function TaskWorkbook() {
return (
<RuneProvider host={taskHost}>
<TaskInput />
<PendingList />
<RuneIntent path="workbook" annotation="mobile, focus on speed" />
</RuneProvider>
);
}
function TaskInput() {
const [newTask, setNewTask] = useSync<string>('new-task'); // ~
const addTask = useAct('add-task'); // !
return (
<div>
<input value={newTask} onChange={e => setNewTask(e.target.value)} />
<button onClick={() => addTask()}>Add</button>
</div>
);
}
function PendingList() {
const pending = useRead<TaskItem[]>('pending'); // @
return <ul>{pending.map(t => <li key={t.id}>{t.title}</li>)}</ul>;
}Pattern 2 โ Fluent host for existing services
function buildRiskHost(riskService: RiskService): RuneHost {
const host = new RuneHost();
host.state.declareComputed('market-price', () => riskService.getMarketPrice());
host.state.declare('risk-threshold', 0.15);
host.recordIntent('risk-threshold',
'approved by risk committee Q1-2025 โ review at quarter end');
host.actions.register('submit-order', (orderId: unknown) =>
riskService.submitOrder(orderId as string));
return host;
}The host wraps the existing service โ riskService never knows about Rune. The governance layer is additive.
Full Source โ
implementations/react/ on GitHub.