Skip to content
HN On Hacker News ↗

Frond — The frontend runtime graph for React apps

▲ 19 points 12 comments by romanonthego 7d ago HN discussion ↗

Pangram verdict · v3.3

We believe that this document is mainly AI-generated, with some AI-assisted content.

88 %

AI likelihood · overall

AI
0% human-written 83% AI-generated
SEGMENTS · HUMAN 0 of 5
SEGMENTS · AI 4 of 5
WORD COUNT 1,186
PEAK AI % 99% · §4
Analyzed
Jul 1
backend: pangram/v3.3
Segments scanned
5 windows
avg 237 words each
Distribution
0 / 83%
human / AI fraction
Verdict
AI
Pangram v3.3

Article text · 1,186 words · 5 segments analyzed

Human AI-generated
§1 AI · 90%

React is not your runtime. Frond is. Your app already has a runtime. It's scattered across providers, effects, and cleanup scripts. Frond makes it a graph. Effect runs it. React stays a renderer. Every growing frontend app arrives at the same problems: how services depend on each other, and what to clean up when the current user changes. Most growing frontend apps hit the same shape. The implementation is a checklist you maintain by hand. Without Frond today / sign-out checklist async function signOut() { await session.end();

// ↓ manually list every user-scoped thing. localStorage.removeItem("token"); queryClient.clear(); // cached queries abortInFlightRequests(); // open fetches presenceChannel.leave(); // realtime presence socket.disconnect(); // realtime transport billingStore.reset(); // domain store navigate("/login");

// added a new user-scoped service? // remember to add a line here too. } Manual memory Every new user-scoped service adds another line to remember. Miss one and the old user can leak through stores, sockets, analytics identity, stale updates, or running requests. With Frond frond / auth action type SessionSpec = Frond.NodeSpec<{ readonly args: Frond.Args.None; readonly key: Frond.Key.Singleton; readonly result: Session; }>;

export class SessionNode extends Frond.NodeBase<SessionSpec> { static readonly spec = Frond.serviceSpec<SessionSpec>({ tag: Frond.tag("app/session"), key: () => Frond.Key.singleton(), driver: Frond.Driver.Async<SessionSpec>({ acquire: Frond.Driver.Acquire(({ signal }) => restoreSession(signal) ), }), }); }

// one call — every dependent node is // evicted, interrupted, and released. function useSignOut() { const controls = FrondReact.useNodeControls(SessionNode, {}); return () => controls.evict("selfAndDependents", "sign-out"); } frond / user-scoped resource type PresenceSpec = Frond.NodeSpec<{ readonly args: Frond.Args.None; readonly key: Frond.

§2 Mixed · 66%

Key.Singleton; readonly deps: { readonly socket: Frond.Dep<typeof SocketNode>; readonly session: Frond.Dep<typeof SessionNode>; }; readonly result: PresenceChannel; }>;

export class PresenceNode extends Frond.NodeBase<PresenceSpec> { static readonly spec = Frond.resourceSpec<PresenceSpec>({ tag: Frond.tag("app/presence"), key: () => Frond.Key.singleton(), dependencies: Frond.dependencies(() => ({ socket: Frond.dep(SocketNode, Frond.Args.none), session: Frond.dep(SessionNode, Frond.Args.none), })), driver: Frond.Driver.Async<PresenceSpec>({ // join the user's presence channel on acquire — // socket heartbeats on its own cadence. acquire: Frond.Driver.Acquire(({ deps }) => deps.socket.result.join("presence", { userId: deps.session.result.userId, heartbeat: 5_000, }) ), // release pairs with acquire — // signOut() never has to know about presence. release: Frond.Driver.Release(({ node }) => node.result.leave({ reason: "sign-out" }) ), }), }); } Runtime boundary Cleanup belongs to the node that acquired the resource. Eviction runs release, cancels in-flight work, clears readiness, and rejects stale commits for the evicted graph record. Read about eviction and release State tools answer Where does the value live? Redux / Zustandvalue, mutation, selector React Queryserver cache, invalidation, retry MobXobservable domain state Contextvalue wiring through React Still outside the model Who owns the lifecycle? What must be ready before this value can load? Which keyed identity is this state attached to? What cancels in-flight work when dependencies change? Who rejects stale commits after eviction? Where do release, telemetry, and reset live? Frond answers When is state allowed to exist? identity observable state dependencies readiness actions scope release eviction Visible state The cache result, observable fields, and computed getters are visible.

§3 AI · 96%

Frond keeps those ergonomics, then attaches them to graph identity, readiness, cancellation, release, and eviction. Runtime lifecycle React reads a node. MobX makes it observable. Effect runs the work. Frond owns when the node is alive, ready, stale, released, or dead. The runtime and its graph backend schema driver return node.result deps.x.result useNode() define / typed driver type ProfileSpec = Frond.NodeSpec<{ readonly args: Frond.Args.None; readonly key: Frond.Key.Singleton; readonly deps: { readonly auth: Frond.Dep<typeof AuthNode>; readonly api: Frond.Dep<typeof ApiNode>; }; readonly result: Profile; }>;

export class ProfileNode extends Frond.NodeBase<ProfileSpec> { static readonly spec = Frond.resourceSpec<ProfileSpec>({ tag: Frond.tag("app/profile"), key: () => Frond.Key.singleton(), dependencies: Frond.dependencies(() => ({ auth: Frond.dep(AuthNode, Frond.Args.none), api: Frond.dep(ApiNode, Frond.Args.none), })), driver: Frond.Driver.Async<ProfileSpec>({ acquire: Frond.Driver.Acquire(async (ctx) => { // ctx.deps.auth.result → AuthState // ctx.deps.api.result → ApiClient return await ctx.deps.api.result.user.profile.query({ userId: ctx.deps.auth.result.userId, signal: ctx.signal, }); }), }), }); } // Profile inferred from driver return — no annotation. depend / types propagate type BillingSpec = Frond.NodeSpec<{ readonly args: Frond.Args.None; readonly key: Frond.Key.Singleton; readonly deps: { readonly profile: Frond.Dep<typeof ProfileNode>; readonly api: Frond.Dep<typeof ApiNode>; }; readonly result: Billing; }>;

export class BillingNode extends Frond.

§4 AI · 99%

NodeBase<BillingSpec> { static readonly spec = Frond.resourceSpec<BillingSpec>({ tag: Frond.tag("app/billing"), key: () => Frond.Key.singleton(), dependencies: Frond.dependencies(() => ({ profile: Frond.dep(ProfileNode, Frond.Args.none), api: Frond.dep(ApiNode, Frond.Args.none), })), driver: billingDriver, });

// no annotation — inferred from dep(ProfileNode). get plan() { return this.deps.profile.result.plan; // ^? Plan } } consume / zero annotations function BillingPage() { // runtime hands a ready BillingNode — // no isLoading, no fallback, no guards. const node = FrondReact.useNode(BillingNode, {});

// node.plan inferred as Plan // through the dep(ProfileNode) chain. return <PlanBadge plan={node.plan} />; } No consumer casts, no manual dependency wiring The graph is the type system. dep(ProfileNode) knows the result type. Dependents inherit it. React reads it. If the driver changes shape, the compiler catches every consumer. Spec and class — how typed nodes work Structured Failures carry kind, tag, retryable, and a cause chain. No e: unknown, no guessing what null means. Walked The runtime walks the chain into a serializable report — fingerprint, tags, contexts, dependency aggregates, runtime event metadata. You don't write the projection. Wired Drop a sink into the runtime once. Every failure routes to your tracker with graph-aware grouping. No per-component try/catch, no remembering to capture. today / catch and reconstruct context // scattered across every fetch, hook, boundary — // each catch builds its Sentry context by hand. async function loadProfile(userId: string) { try { return await api.getProfile(userId); } catch (e) { Sentry.captureException(e, { tags: { feature: "profile" }, // is it readiness? auth? // a flattened DependencyFailed? // we only have `e: unknown`.

§5 AI · 99%

// no chain (lost three try/catches ago) // no retryable flag // no consistent fingerprint }); throw e; } }

// repeat for billing.ts, // feed.ts, dashboard.ts, ... frond / one sink, walked chain // One sink. Every failure in every node // flows to Sentry with graph-aware grouping. // (Or any tracker — the report shape is generic.)

const sentrySink = Frond.Diagnostics.createRuntimeReportSink({ name: "sentry", handleReport: ({ report }) => { Sentry.captureException(report.error, { fingerprint: [...report.fingerprint], // ["frond", kind, rootTag, nodeTag] tags: report.tags, // { "frond.kind", "frond.retryable", // "frond.root_tag", "frond.node_tag" } contexts: report.contexts, // { frond, causeChain, dependencyFailures, // runtimeEvent } extra: report.extra, }); }, });

const runtime = Frond.createRuntime({ sinks: [sentrySink], }); Errors are part of the model The runtime classifies, walks the cause chain, and builds a report shaped for Sentry-style trackers — fingerprint groups by graph topology, tags carry kind and retryable, contexts carry the full chain. Wire it once. How errors flow through the graph Cancellation Signals everywhere Every acquire and refresh receives a signal wired to its scope. When a node evicts, in-flight work is interrupted — fetches abort, timers clear, streams close. Scoped resources Cleanup runs in reverse Sockets, subscriptions, intervals — register them with disposers.add(...). Release runs them in reverse order, on the runtime path. Composable failure Throw, propagate, structure A driver throws. The runtime catches, classifies, attaches the cause chain, and notifies every dependent. The runtime uses the same cause-chain reporting shown above. Opt in Write the orchestration you'd write anyway. Swap Frond.Driver.Async for Frond.Driver.Effect and you get retry, bounded concurrency, timeouts, and declarative failure classification — composed, not hand-rolled.