Frond — The frontend runtime graph for React apps
v0 · Developer Preview Frond is under active development. APIs may change between releases.<br>React is not your runtime. Frond is.
Your app already has a runtime. It's scattered across providers, effects, and cleanup scripts.<br>Frond makes it a graph.<br>Effect runs it.<br>React stays a renderer.
Get started
View on GitHub
runtime graph — click a node to inspect
env id: env:appserviceready
session id: session:currentserviceready
telemetry id: telemetry:sdkserviceready
httpTransport id: httpTransport:clientserviceready
socketTransport id: socketTransport:socketserviceready
profile id: profile:currentresourceready
billing id: billing:accountresourceready
feed id: feed:streamresourceready
dashboard id: dashboard:screenfacadeready
React Flow<br>Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.<br>Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.
frondtoday<br>type ProfileSpec = Frond.NodeSpec;<br>};<br>readonly result: Profile;<br>}>;
export class ProfileNode extends Frond.NodeBase {<br>static readonly spec = Frond.resourceSpec({<br>tag: Frond.tag("app/profile"),<br>key: () => Frond.Key.singleton(),<br>dependencies: Frond.dependencies(() => ({<br>http: Frond.dep(HttpTransportNode, Frond.Args.none),<br>})),<br>driver: Frond.Driver.Async({<br>// Cannot load before http is ready.<br>// Eviction on sign-out runs through the same disposers.<br>acquire: Frond.Driver.Acquire(({ deps, signal }) =><br>deps.http.result.getProfile(signal)<br>),<br>}),<br>});
// Derived view fields stay close to the data, not the screen.<br>get displayName(): string {<br>return this.result.displayName;
get initials(): string {<br>return initialsFromName(this.result.displayName);
loading runtime graph
Every growing frontend app arrives at the same problems:<br>how services depend on each other, and what to clean up when the current user changes.<br>Most growing frontend apps hit the same shape. The implementation is a checklist you maintain by hand.
Without Frond<br>today / sign-out checklist<br>async function signOut() {<br>await session.end();
// ↓ manually list every user-scoped thing.<br>localStorage.removeItem("token");<br>queryClient.clear(); // cached queries<br>abortInFlightRequests(); // open fetches<br>presenceChannel.leave(); // realtime presence<br>socket.disconnect(); // realtime transport<br>billingStore.reset(); // domain store<br>navigate("/login");
// added a new user-scoped service?<br>// remember to add a line here too.
Manual memory<br>Every new user-scoped service adds another line to remember. Miss one and the old<br>user can leak through stores, sockets, analytics identity, stale updates, or running requests.
With Frond
session id: session:currentrootready
transport id: transport:clientserviceready
socket id: socket:clientserviceready
profile id: profile:currentresourceready
billing id: billing:accountresourceready
presence id: presence:userresourceready
React Flow<br>Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.<br>Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.
⚡ evict user scope<br>frond / auth action<br>type SessionSpec = Frond.NodeSpec<br>readonly args: Frond.Args.None;<br>readonly key: Frond.Key.Singleton;<br>readonly result: Session;<br>}>;
export class SessionNode extends Frond.NodeBaseSessionSpec> {<br>static readonly spec = Frond.serviceSpecSessionSpec>({<br>tag: Frond.tag("app/session"),<br>key: () => Frond.Key.singleton(),<br>driver: Frond.Driver.AsyncSessionSpec>({<br>acquire: Frond.Driver.Acquire(({ signal }) =><br>restoreSession(signal)<br>),<br>}),<br>});
// one call — every dependent node is<br>// evicted, interrupted, and released.<br>function useSignOut() {<br>const controls = FrondReact.useNodeControls(SessionNode, {});<br>return () => controls.evict("selfAndDependents", "sign-out");
frond / user-scoped resource<br>type PresenceSpec = Frond.NodeSpec<br>readonly args: Frond.Args.None;<br>readonly key: Frond.Key.Singleton;<br>readonly deps: {<br>readonly socket: Frond.Deptypeof SocketNode>;<br>readonly session: Frond.Deptypeof SessionNode>;<br>};<br>readonly result: PresenceChannel;<br>}>;
export class PresenceNode extends Frond.NodeBasePresenceSpec> {<br>static readonly spec = Frond.resourceSpecPresenceSpec>({<br>tag: Frond.tag("app/presence"),<br>key: () => Frond.Key.singleton(),<br>dependencies: Frond.dependencies(() => ({<br>socket: Frond.dep(SocketNode, Frond.Args.none),<br>session: Frond.dep(SessionNode, Frond.Args.none),<br>})),<br>driver: Frond.Driver.AsyncPresenceSpec>({<br>// join the user's presence channel on acquire —<br>// socket heartbeats on its own cadence.<br>acquire: Frond.Driver.Acquire(({ deps }) =><br>deps.socket.result.join("presence", {<br>userId: deps.session.result.userId,<br>heartbeat: 5_000,<br>})<br>),<br>// release pairs with acquire —<br>// signOut() never has to know about presence.<br>release: Frond.Driver.Release(({ node }) =><br>node.result.leave({ reason:...