@s-age/react-kernelee
v0.3.0
Published
React bindings for kernelee's Buffer — useBuffer/useDispatch/useTrigger/useKernel/useKernelError + KernelProvider
Maintainers
Readme
react-kernelee
React bindings for kernelee's Buffer — the observable-state
half of the kernel. Independent plugin package: kernelee has no dependency
on this package, or on React at all.
npm test # vitest run
npm run typecheck # tsc --noEmit
npm run build # tsc -p tsconfig.build.json → dist/ (declarations included)Dependency direction
react-kernelee → kernelee
↑
(your app)kernelee never imports React, and this package never modifies kernelee —
it only consumes the public surface documented in kernelee's own README:
Kernel, StateKey<S>, Buffer.subscribe / Buffer.getSnapshot,
KernelSymbol<P, O>, and KernelErrorState. The kernelee core knows
nothing about React — it is a plain TS core usable from a CLI, a server, a Swift-adjacent test
harness, or (via this package) a React tree; nothing about its design assumes
a UI framework.
Usage
Compose the kernel once, at the app root, exactly as a non-React consumer
would — then inject it with KernelProvider:
import { createRoot } from 'react-dom/client';
import { KernelBuilder, BufferBuilder } from '@s-age/kernelee';
import { KernelProvider } from '@s-age/react-kernelee';
import { App } from './App.js';
const bufferBuilder = new BufferBuilder();
// ...bufferBuilder.allocate(MyState), ...builder.register(...), wiring, etc.
const kernel = new KernelBuilder().build({ buffer: bufferBuilder });
createRoot(document.getElementById('root')!).render(
<KernelProvider kernel={kernel}>
<App />
</KernelProvider>,
);Inside the tree, components read state and fire commands — nothing else:
import { useBuffer, useTrigger, useKernelError } from '@s-age/react-kernelee';
import { GridState, refreshGrid } from './contract.js';
function GridView() {
const grid = useBuffer(GridState); // re-renders on every mutate
const refresh = useTrigger(refreshGrid); // stable fn, fire-and-forget
const error = useKernelError(); // KernelErrorState.message sugar
return (
<div>
{error && <p role="alert">{error}</p>}
<button onClick={refresh}>Refresh</button>
{/* render grid */}
</div>
);
}API
<KernelProvider kernel={kernel}>{children}</KernelProvider>— injects aKernelvia React context. Mount once, at (or near) the composition root. Nested providers shadow the outer kernel for their subtree (useful in tests: wrap a component under test with its own throwaway kernel).useKernel(): Kernel— reads the kernel injected by the nearestKernelProvider. Throws a clear error naming the missing provider when called outside one. Most components should preferuseBuffer/useDispatch/useKernelError;useKernelis the escape hatch for code that needs the kernel handle directly.useBuffer(key: StateKey<S>): S— subscribes to one buffer cell and re-renders on everymutate. Wrapskernel.buffer.subscribe/kernel.buffer.getSnapshotinuseSyncExternalStore, withsubscribe(andgetSnapshot) stabilized viauseCallbackso identity only changes whenkernelorkeychange — otherwiseuseSyncExternalStorewould tear down and re-establish the subscription on every render. The samegetSnapshotdoubles as thegetServerSnapshotargument (a plain synchronous read has no browser-only dependency, so it is SSR-safe as is). ThrowsBufferError('unallocated') ifkeywas never allocated — same as callingkernel.buffer.read(key)directly.useDispatch(sym: KernelSymbol<P, O>): (payload: P) => void— binds a non-void-payload symbol to a component-stable dispatcher for event handlers.dispatchis already fire-and-forget and forward-only (no return value; failures go to the error sink, never to the caller); this hook only gives the call a stable identity viauseCallback.void-payload symbols are rejected at compile time — useuseTriggerinstead.useTrigger(sym: KernelSymbol<void, O>): () => void— the sole binding form forvoid-payload commands. Returns a stable zero-argument dispatcher that forwards nothing, so handing it toonClicket al. can never leak the event object into the dispatched payload.The exclusive three-way split for binding a command to a handler:
void→useTrigger/ non-void→useDispatch(sym)/ action →useDispatch().useKernelError(): string | null— sugar foruseBuffer(KernelErrorState).message. Observes the default error sink only: an app that injects its ownonErroratbuild()replaces that sink entirely, so this hook always readsnullin that case — read your own error state viauseBufferinstead.It only reads; clearing is the displaying view's job through the normal dispatch path, not a second hook:
const message = useKernelError(); const dispatch = useDispatch(); // or useTrigger(clearError) for a bare symbol // <button onClick={() => dispatch(FaultsActions.clearError())}>dismiss</button>the handler side
mutatesKernelErrorStateback to{ message: null }(see the kernelee README's dispatch/onError recipe).
View-layer discipline
A component only reads (useBuffer, or its sugar useKernelError) and
dispatches (useDispatch). It never calls kernel.call, kernel.compose,
kernel.run, or kernel.buffer.mutate directly — those belong to the
Circuit/Compute-equivalent layer that owns transition logic and writes the
buffer. useKernel() exists for the rare case a component needs the raw
kernel handle, but reaching for it to call/compose/mutate inline reintroduces
the coupling this package's other hooks exist to avoid.
License
MIT © s-age
