Show HN: Frond – a frontend runtime for your app's dependency graph

romanonthego1 pts0 comments

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

frond readonly session driver result node

Related Articles