v0 · Developer Preview Frond is under active development. APIs may change between releases.

Wire it to React

The runtime holds the graph; React reads it. Each hook is a subscription against the live runtime, so a component does not fetch, track loading state, or clean up after itself.

This page reads the ProfileNode from your first node inside a real component.

Nodes are MobX-backed. Wrap components in observer from mobx-react-lite when they render node fields, getters, or result-backed data. Without observer, Suspense, error boundaries, controls, and hook lifecycle state still work, but MobX field changes and live observation will not reliably update or register. Components that only drive controls don’t need it.

The provider

You mounted it in Install. Here it is again.

import * as FrondReact from "@frondruntime/react";
import { runtime } from "./runtime";

export function App() {
  return (
    <FrondReact.FrondProvider runtime={runtime}>
      <ProfilePanel userId="u_42" />
    </FrondReact.FrondProvider>
  );
}

One provider, at the root. Every hook below reads from this runtime.

useNode — the common case

useNode acquires a node and gives you the ready instance. It suspends until ready and throws to the nearest error boundary on failure.

import * as FrondReact from "@frondruntime/react";
import { observer } from "mobx-react-lite";
import { ProfileNode } from "./nodes/profile";

const ProfilePanel = observer(({ userId }: { userId: string }) => {
  const profile = FrondReact.useNode(ProfileNode, { userId });

  return <h1>{profile.displayName}</h1>;
});

The component has no if (loading), if (error), or useEffect. The Suspense boundary above handles loading; the error boundary above handles failure.

import { Suspense } from "react";
import { ErrorBoundary } from "./your-error-boundary";

<ErrorBoundary fallback={<p>Couldn't load profile.</p>}>
  <Suspense fallback={<p>Loading…</p>}>
    <ProfilePanel userId="u_42" />
  </Suspense>
</ErrorBoundary>

If userId changes, the hook switches to the new node — same identity rules as outside React. Two <ProfilePanel userId="u_42" /> mounted in different places share the same instance.

useNodeState — when you need the lifecycle

useNode returns only the ready node. To render while the node is busy (a refresh spinner over the current value, say), or to distinguish stale data from current, use useNodeState. It suspends and throws readiness errors exactly like useNode — in fact useNode returns useNodeState(...).node — but it also hands back the node’s live operation state.

const ProfilePanel = observer(({ userId }: { userId: string }) => {
  const state = FrondReact.useNodeState(ProfileNode, { userId });

  return (
    <div>
      <h1>{state.node.displayName}</h1>
      {state.busy && <span className="spinner" aria-label="Refreshing" />}
    </div>
  );
});

What you get back:

  • state.node — the ready node instance. The hook suspends until the node is ready, so state.node is always ready when the component renders.
  • state.operation — the running operation: { _tag: "Idle" } or { _tag: "Running", kind, ... } where kind is "action", "refresh", or "args".
  • state.busytrue while any operation is running. Spinner predicate.
  • state.operationFailure — the last operation failure, if any.
  • state.resultValidity{ _tag: "Current" | "Stale" }. Useful for “loaded an hour ago, refreshing now” UI.

Use useNode when you want the simple ready-or-Suspense path. Use useNodeState when render needs to know the lifecycle.

Preload — acquire before render

Preload acquires a set of nodes before its children render. Useful at route boundaries — the route doesn’t paint until its data is ready.

<FrondReact.Preload nodes={[{ profile: [ProfileNode, { userId: "u_42" }] }]}>
  <ProfilePanel userId="u_42" />
</FrondReact.Preload>

Preload suspends just like useNode. It doesn’t pin nodes alive on its own — it acquires them, then the children read them. Use it to consolidate Suspense boundaries above mixed-data screens.

Controls without rendering

If a component needs to refresh or evict a node without reading its result, use useNodeControls. It doesn’t suspend.

function RefreshButton({ userId }: { userId: string }) {
  const controls = FrondReact.useNodeControls(ProfileNode, { userId });

  return <button onClick={() => void controls.refresh()}>Reload</button>;
}

controls.refresh(), controls.evict(), controls.releaseResources() — runtime-level operations. For product behavior (like incrementing a counter or saving a form), call node actions instead — those live on the node instance you read with useNode.

What the runtime handles

These are the runtime’s responsibility, not the component’s:

  • Fetching — no useEffect(() => { fetch(...); return cleanup; }, [userId]).
  • Loading, error, and data state — no useState for them.
  • Request cancellation — no manual AbortController.
  • De-duplication — two components reading the same user share one fetch.
  • Refresh and invalidation — driven through the node, not a separate library.

The React entry points are the provider, the hooks (useNode, useNodeState, useNodeControls, and the map variants), and Preload.


You now have a runtime, a node, and a component reading it. The rest of the docs cover the model (eviction cascades, subgraphs), the authoring surface (services, facades, drivers, actions), and recipes (Sentry projection, pagination, Effect escape hatches).