← All guided builds

Guided build · 21

Take an App Builder contract to React or Vue

Replace the generated renderer while preserving the App's exact model reads, writes, routes, error behavior, and capability boundary.

You will finish with: A contract-checked React or Vue approval queue that runs through Grid's generated connector and survives App Builder regeneration.

35 minAdvancedSource checked for Grid 0.61.0Reviewed 2026-08-25
Contract checkcontract-check.mjsA startup assertion that rejects read, write, route, or capability drift from the tutorial baseline.
React custom entryreact-approval-queue.jsxA React approval queue using the generated useGridApp adapter and exact model-owned state.
Vue custom entryvue-approval-queue.jsA Vue approval queue using the generated composable and the same contract as the React entry.
On this page

What you will build

A hand-authored React or Vue approval queue that keeps the model contract generated by App Builder. The screen will subscribe to the same six model reads, write only the same four inputs, keep the same three routes, and surface connection or write failures instead of silently falling back to local state.

This is a package handoff, not a rewrite of the Grid model. Complete Build your first Grid app first, or download its Completed app and deploy it. You also need a package workspace or host workflow that can build and republish a model-owned UI package. The browser builder generates and publishes the package, but it is not a general-purpose React or Vue project editor.

The downloads for this tutorial contain:

  • Contract check, a startup assertion for the approval app's reads, writes, action routes, and capabilities;
  • React custom entry, a replacement for custom-ui/main.jsx;
  • Vue custom entry, a replacement for custom-ui/vue-main.js.

Choose React or Vue for the implementation. Use the contract check with either one.

1. Publish a known App Builder baseline

Open order-operations-app.grid, deploy the model, select Approval_App, and choose Publish Grid App. Do this before customizing so the handoff starts from a package generated from the current App document—not an older copy of its contract.

In App framework, verify the baseline:

Contract Expected value
Pages Home, Requests, Review
Reads Decision, Feedback, Requests, SearchRequests, SelectedRequest, TotalRequested
Writes Decision, Feedback, SearchRequests, SelectedRequest
Capabilities app.route, model.read, model.write
Home dist/approval-queue

Checkpoint: the package opens in App Mode, request selection survives a route change, and the approval action writes Decision and Feedback before returning to Requests.

The generated write list is an allowlist, not ownership. The four write targets also remain declared as model input bindings. Custom code does not gain permission to mutate Requests or TotalRequested.

2. Inspect the generated handoff

The published grid.model-ui.v1 package contains the generated renderer and a separate custom project:

File Purpose
dist/app-meta.json Machine-readable pages, actions, bindings, features, and capability review.
dist/GRID_APP.md Human-readable model and package contract.
dist/grid-app.js App-specific constants, typed guards, and action helpers.
dist/grid-react.js React useGridApp adapter.
dist/grid-vue.js Vue useGridApp composable.
dist/grid-connector.js Shared grid:model-ui:v1 connector and reactive state store.
custom-ui/manifest.json Handoff inventory for tools and agents.
custom-ui/main.jsx Generated React starting point.
custom-ui/vue-main.js Generated Vue starting point.
custom-ui/grid-app.d.ts App-specific symbol and helper types.
custom-ui/index.html Candidate replacement entry. It is inactive until the package entry changes.

Keep the dist/grid-* files generated. They contain the connector handshake, subscription store, write-symbol checks, route bridge, and Dev mode bridge. A custom screen should use those adapters instead of opening its own transport or calling the model runtime directly.

The active package entry is still dist/index.html at this point.

3. Freeze the contract at the handoff boundary

Copy the downloaded contract-check.mjs into custom-ui/. Both sample entries call:

import { APP_CONTRACT } from "../dist/grid-app.js";
import { APPROVAL_ROUTES, assertApprovalContract } from "./contract-check.mjs";

assertApprovalContract(APP_CONTRACT);

The assertion compares binding and capability sets without depending on array order. It also freezes the home path and the four generated action plans: kinds, targets, reads, writes, success routes, feedback targets, and action capabilities. It fails when any of those change. That failure is intentional: return to App Builder, review the changed tree and model ownership, republish, and then make an explicit contract change in custom code.

Do not copy the expected binding arrays into a second runtime client. APP_CONTRACT, APP_READ_SYMBOLS, APP_WRITE_SYMBOLS, and APP_WATCHED_SYMBOLS from dist/grid-app.js remain the live generated authority. The guard exports APPROVAL_ROUTES so the custom renderer and its action assertions share one route baseline. The local assertion is only a deployment guard for this tutorial's known package.

Checkpoint: changing Decision to an undeclared write symbol in the check makes the custom entry fail before it can render or issue a write.

4. Implement the React entry

For React, replace the generated custom-ui/main.jsx with the downloaded react-approval-queue.jsx. Its important seam is the generated hook:

import { APP_CONTRACT, APP_TITLE } from "../dist/grid-app.js";
import { useGridApp } from "../dist/grid-react.js";
import { APPROVAL_ROUTES } from "./contract-check.mjs";

function ApprovalQueue() {
  const grid = useGridApp();
  const requests = grid.rows("Requests");
  const selected = grid.value("SelectedRequest", "");

  async function openRequest(requestId) {
    await grid.set("SelectedRequest", requestId, "string");
    await grid.route(APPROVAL_ROUTES.review);
  }
}

useGridApp starts and stops the subscription with the component, returns a reactive snapshot, and routes every write through assertAppWriteSymbol. The example deliberately keeps SearchRequests in the model rather than replacing it with component-only state. That preserves the observable app seam used by other surfaces and formulas.

The approval path uses one batch:

await grid.setBatch([
  { symbol: "Decision", value: "approved", typeTag: "string" },
  { symbol: "Feedback", value: "Request approved.", typeTag: "string" },
]);

Keep write and route errors visible. An optimistic custom screen that reports success before the host accepts a write hides ownership and capability failures.

5. Or implement the Vue entry

For Vue, replace custom-ui/vue-main.js with the downloaded vue-approval-queue.js. Update the replacement HTML or your build entry so it loads the Vue file instead of main.jsx.

The Vue adapter exposes the same contract through Vue reactivity:

import { computed } from "vue";
import { useGridApp } from "../dist/grid-vue.js";
import { APPROVAL_ROUTES } from "./contract-check.mjs";

setup() {
  const grid = useGridApp();
  const requests = computed(() => grid.rows("Requests"));
  const selected = computed(() => grid.value("SelectedRequest", ""));

  async function openRequest(requestId) {
    await grid.set("SelectedRequest", requestId, "string");
    await grid.route(APPROVAL_ROUTES.review);
  }

  return { requests, selected, openRequest };
}

Call useGridApp from setup() so its subscription uses Vue's mount and unmount lifecycle. Do not create a second global connector alongside it. The sample uses computed values over the adapter's reactive snapshot and the same batch approval operation as the React entry. Its downloadable screen uses Vue's h() render API, so it works with the standard runtime-only Vue build and does not depend on an in-browser template compiler.

Checkpoint: React and Vue observe and write the same symbols. Framework choice changes rendering, not the model or authorization contract.

6. Build a browser entry without breaking the package layout

The downloaded React file contains JSX and the Vue file imports Vue. They are source files, not a production browser bundle. Use your approved package build to compile the chosen entry and include its framework runtime.

Preserve these boundaries in the build:

  1. Keep imports from ../dist/grid-app.js and the matching adapter resolvable at runtime, or bundle those generated modules without removing their write guards.
  2. Emit the browser-ready custom JavaScript inside the package; do not point production HTML at uncompiled JSX.
  3. Keep custom-ui/index.html's #app mount element and the generated dist/styles.css, or replace both deliberately.
  4. Keep the package manifest's bindings and requested capabilities unchanged for this build.
  5. Change the package entry from dist/index.html to custom-ui/index.html only after the custom entry loads successfully in the package shell.

Alternatively, keep dist/index.html as the entry and replace its compiled dist/app.js. That avoids an entry-path change but makes it especially important to keep generated and authored build outputs separate.

Never edit only the coarse model.write capability. The package must preserve the exact write symbol list as well; the host enforces that narrower list independently.

7. Test against the real Grid connector

A custom package should be tested inside App Mode or a host-provided package harness. Opening its HTML directly from disk does not provide the grid:model-ui:v1 parent connector, so a failed handshake there is expected rather than proof that the model is broken.

Use this minimum matrix:

Case Expected result
Connect The initial six watched symbols resolve; the loading state clears.
Live update A model-side change to Feedback appears without reloading the app.
Empty A search with no matches renders “No matching requests.”
Error A rejected or unavailable model operation remains visible near the action.
Select and route Review writes SelectedRequest, then routes to dist/approval-queue/review.
Batch approval Decision and Feedback update together; success routes back to Requests.
Forbidden write grid.set("Requests", ...) is rejected by the generated app guard.
Deep link Opening each of the three package paths resolves the intended screen.
Responsive Queue and action controls remain usable at desktop, tablet, and mobile widths.
Escape The package's Dev mode control or Shift+Esc, Cmd+Esc, or Ctrl+Esc returns to the workbench.

Also compare the final package against the baseline contract:

reads:        Decision, Feedback, Requests, SearchRequests, SelectedRequest, TotalRequested
writes:       Decision, Feedback, SearchRequests, SelectedRequest
capabilities: app.route, model.read, model.write

Do not grant job.run or connector.call merely because custom code could call those adapter methods. Add a capability only when the workflow needs it, the model and host expose it, and the review names the exact action.

8. Publish and keep regeneration safe

Publish the tested package through the model UI package workflow used by your Grid host, then open the model's App Home in a fresh session. Verify the package revision, home route, reads, writes, and requested capabilities in the publish review or stored manifest.

Publish Grid App in App Builder generates a fresh package from the current App tree. Treat it as regeneration: keep the custom React or Vue source and build configuration in version control, and make reapplying the custom entry part of the package pipeline. Do not leave the only custom copy inside generated output that a later App Builder publish can replace.

Publishing the UI package does not publish a grid namespace release, change model access policy, or deploy this documentation site. Those remain separate operations.

You are done when

  • the custom entry uses Grid's generated React or Vue adapter;
  • the generated contract assertion passes at startup;
  • reads, writes, routes, and capabilities match the App Builder baseline;
  • loading, empty, error, live-update, write-rejection, route, and responsive cases pass;
  • the browser entry is compiled rather than raw JSX;
  • the package opens as App Home and still returns to Dev mode;
  • authored custom source survives future App Builder regeneration through version control and the package build pipeline.

Return to Build Grid apps for the architecture and trust comparison, or Publish to a grid namespace when the model itself also needs a versioned namespace release.

Build statusReached the expected checkpoint?