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)