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

Hacker News Top Tools

Summary

Frond is a frontend runtime that manages the dependency graph of React apps, handling service lifecycles and cleanup automatically.

No content available
Original Article
View Cached Full Text

Cached at: 07/01/26, 02:00 PM

# Frond — The frontend runtime graph for React apps Source: [https://frondruntime.dev/](https://frondruntime.dev/) ## Reactis notyour runtime\.Frondis\. Your app already has a runtime\. It's scattered across providers, effects, and cleanup scripts\.Frondmakes it a graph\.Effectruns it\.Reactstays 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 memoryEvery 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.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 boundaryCleanup 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](https://frondruntime.dev/docs/model/eviction-and-release) State tools answer ### Where does the value live? Redux / Zustandvalue, mutation, selectorReact Queryserver cache, invalidation, retryMobXobservable domain stateContextvalue 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 stateThe cache result, observable fields, and computed getters are visible\. Frond keeps those ergonomics, then attaches them to graph identity, readiness, cancellation, release, and eviction\. Runtime lifecycleReact 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](https://frondruntime.dev/docs/model/runtime-and-graph) 1. backend schema 2. driver return 3. node\.result 4. deps\.x\.result 5. 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.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 wiringThe 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](https://frondruntime.dev/docs/authoring/spec-and-class) 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`. // 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 modelThe 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](https://frondruntime.dev/docs/authoring/errors) 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\. Retry`Schedule\.exponential`vs\. your own backoff loop\.Concurrency`Effect\.all\(\{ concurrency \}\)`vs\. your own Promise gate\.Classification`while: \(e\) =\> …`vs\. nested`if/else`in catch\. frond / effect\-mode driver, retry \+ concurrency \+ classify ``` // DashboardSpec: facade, api dep, three-panel result. export class DashboardNode extends Frond.NodeBase<DashboardSpec> { static readonly spec = Frond.facadeSpec<DashboardSpec>({ tag: Frond.tag("app/dashboard"), key: () => Frond.Key.singleton(), dependencies: Frond.dependencies(() => ({ api: Frond.dep(ApiNode, Frond.Args.none), })), driver: Frond.Driver.Effect<DashboardSpec>({ acquire: Frond.Driver.Acquire((ctx) => Effect.gen(function* () { const fetchPanel = (panel: PanelId) => ctx.tryPromise((signal) => ctx.deps.api.result.dashboard.panel(panel, signal) ).pipe( // exponential backoff, fail fast on auth. Effect.retry({ schedule: Schedule.exponential("100 millis"), times: 3, while: (e) => e._tag !== "AuthError", }), Effect.timeout("5 seconds"), ); // three panels in parallel, two in flight at a time. const [activity, billing, feed] = yield* Effect.all( [fetchPanel("activity"), fetchPanel("billing"), fetchPanel("feed")], { concurrency: 2 } ); return { activity, billing, feed }; }) ), }), }); } ``` Effect is the engine, not the APIYou get cancellation, scopes, and structured failure without writing a single`Effect\.gen`\. The escape hatch is there if you want it\. [Drivers and Effect mode](https://frondruntime.dev/docs/authoring/drivers) Probably not - Your app mostly renders independent screens\. - Data loading is local to a page\. - Logout clears one token and one cache\. - React Query explains most async state\. - You do not have long\-lived frontend services\. Probably yes - Startup has real readiness gates\. - Services depend on other services\. - User identity invalidates half the app\. - Sockets, SDKs, analytics, and transports need cleanup\. - Screens aggregate many resources\. - You need to know why something is not ready\. [Author your first node](https://frondruntime.dev/docs/start/first-node)

Similar Articles

Any Custom Frontend with Gradio's Backend

Hugging Face Blog

Hugging Face introduces `gradio.Server`, a new tool that allows developers to use Gradio's backend infrastructure (queuing, hosting) with custom frontends built using React, Svelte, or plain HTML/CSS/JS.

Show HN: Freenet, a peer-to-peer platform for decentralized apps

Hacker News Top

Freenet is a peer-to-peer platform for building decentralized applications that run without servers, using a small-world network organized by location on a ring. It allows developers to deploy apps using familiar tools like Rust and TypeScript, and users access them via browser without tracking or takedown risk.