Skip to main content

When a user does something — connects a service, completes a task, sends a message — the screen should reflect it instantly, with no refresh button, no reopening the screen, and no hand-written copy of the new state. This page shows the one loop that makes that happen.

The product moment

A user clicks Connect on a service tile. The credential is saved. The tile should flip to Connected the moment the save lands — on its own.The wrong way to get there is to fake it: flip the tile in component state because you “know” the connect succeeded, or re-pull the whole feed every time the screen mounts. Both drift from the real data, and both are extra code you later have to delete. The right way is a single reactive loop where the rendered screen is always a direct projection of the saved data.

The loop

act

A surface asks a target to do work with sendRequestToBitfieldTarget(…).

change

The target writes the package records the prepared value is built from.

rebuild

A change-driven rule re-prepares the named value from the new records.

render

useBitfieldData(…) receives the new value and React re-renders.

Every arrow is automatic once the rule exists. The component does not poll, does not re-fetch on mount, and never holds a second copy of the truth.

The read side is already live

useBitfieldData(...) is not a one-time fetch. It is a live subscription to a named value. When that value changes, every component reading it re-renders with the new value.
import { useBitfieldData } from '@bitfield/runtime-kit/react';

type ConnectionRow = { id: string; name: string; connected: boolean };

export function ConnectionList() {
  const feed = useBitfieldData<ConnectionRow[]>('service-connections');

  if (feed.loading) return <section>Loading connections.</section>;
  if (feed.error) return <section>Could not load connections.</section>;
  if (!feed.data) return <section>No connections yet.</section>;

  return (
    <section>
      {feed.data.map((row) => (
        <p key={row.id}>{row.name}{row.connected ? 'Connected' : 'Not connected'}</p>
      ))}
    </section>
  );
}
You never call a “refresh” function here. If the named value changes, this list updates by itself. So the real question is not “how do I refresh the component?” It is “what makes the named value change?”

The write side changes the records

A user action asks a target to do work. That target writes the package records the named value is prepared from.
import { sendRequestToBitfieldTarget } from '@bitfield/runtime-kit';

async function connect(serviceId: string) {
  await sendRequestToBitfieldTarget({
    target: 'service-connections.connect',
    payload: { serviceId },
  });
}
This is the part that matters most: the write must change the same records the prepared value is built from. If your screen shows a value derived from records A, B, and C, then a connect that only writes record D will never change the screen — no matter how reactive the rest of the loop is. The write and the projection must agree on which records carry the truth.Between “records changed” and “named value changed” sits the piece people forget. The prepared value does not rebuild itself just because some record changed somewhere. A package declares a reactive rule that watches the records the value depends on and re-prepares the value when any of them change.State the rule with three facts:
Part of the ruleWhat you provide
What to watchThe records (or the fields) the prepared value is built from.
When to fireOn a change to any of those watched records.
What to doRe-prepare the named value the surface reads.
Declare this once, as package data. After that the loop is closed: a write changes a watched record, the rule fires, the named value is rebuilt, and every subscribed component re-renders. No surface code runs the rebuild, so no surface can forget to.The rule lives next to the package data, not in the component. That separation is the whole point — the screen stays a dumb projection, and the reactive behavior is owned by the package that owns the data.

Make the rule and the projection match

The most common reason a correct-looking loop still shows stale state: the reactive rule watches one set of records, but the prepared value is built from a different set. The write updates records the rule does not watch, or the rule rebuilds from records the write does not touch. Keep one answer to “which records carry this truth,” and point both the rule and the projection at it.

Anti-patterns

These three shortcuts all look like they work and all fight the loop. Delete them.Optimistic local copy. The component keeps its own connectedIds set and flips a row to connected because the request resolved. Now there are two truths — the local set and the prepared value — and they drift the first time a write partly fails or another surface changes the same data. Let the write change the records and let the rule rebuild the value. The render is the truth.Re-pull on mount. The surface rebuilds the whole feed in a mount effect “so it is fresh when the screen opens.” This hides a missing reactive rule: it makes opening the screen the refresh, instead of the data change being the refresh. It also does nothing while the screen is already open. Remove the mount effect and add the rule.Manual rebuild after every write. Each action calls the rebuild itself right after writing. This works until one new caller forgets, and it couples every writer to the projection. One reactive rule replaces every one of these calls and can never be forgotten.

What this prevents

BugCause it removes
Tile flips, then snaps back.Optimistic local copy drifting from the rebuilt value.
Screen only correct after reopening.A missing reactive rule masked by a mount-time re-pull.
One screen updates, another does not.A writer that forgot its manual rebuild call.
Action succeeds but nothing visibly changes.The write changed records the projection does not read.

Verify

CheckPass condition
Live readThe component reads the named value with useBitfieldData(...) and renders loading, error, empty, and data states.
Write targetThe user action calls a target with sendRequestToBitfieldTarget(...); it changes the records the value is built from.
Reactive ruleA package-declared rule rebuilds the named value when those records change.
No second truthThe component holds no optimistic copy, no mount-time re-pull, and no manual rebuild call.
Instant updateDoing the action updates the open screen with no refresh and no reopen.

What should happen

The user does the action once.
The records change.
The prepared value rebuilds on its own.
Every screen reading that value updates instantly.
No component holds a private copy of the new state.

Next

Read Read data in React for the read hook contract and selectors.Read Send a request for payloads, replies, and request errors on the write side.Read React surface for package data for a full read-plus-request surface to attach this loop to.Read Package to screen for the full path from package data to a rendered surface.Read Runtime Kit API for the exact useBitfieldData(...) and sendRequestToBitfieldTarget(...) contracts.
Last modified on June 30, 2026