Skip to main content

The React Renderer

@ausus/renderer-react is the React client for AUSUS. It fetches a ViewSchema from the HTTP API and renders it. It is the L6 layer of the stack.

React is treated as a rendering engine only — the renderer holds no domain knowledge. Everything it draws comes from the ViewSchema.

Install

npm install @ausus/renderer-react react@18 react-dom@18
# React 19 is also supported:
# npm install @ausus/renderer-react react@^19 react-dom@^19

react and react-dom are peer dependencies (^18 || ^19). The package is ESM-only and ships no bundled dependencies.

Public API

import {
AususProvider, useAusus,
useViewSchema, useAction,
ViewSchemaConsumer,
ListView, DetailView, ActionModal, WorkflowBadge, FieldDisplay,
inputDefault, isRequired, shapeValue, validateInputs,
} from "@ausus/renderer-react";
ExportKindPurpose
AususProvidercomponentinjects API base URL, tenant, and fetcher
useAusushookreads the provider context
useViewSchemahookfetches a projection's ViewSchema
useActionhookinvokes an action
ViewSchemaConsumercomponentfetches a projection and dispatches to a view
ListView / DetailViewcomponentsrender a list / detail ViewSchema
ActionModalcomponentconfirmation + dynamic input form for an action
WorkflowBadgecomponentcolored badge for a workflow state
FieldDisplaycomponentrenders one field cell by type
inputDefault, isRequired, shapeValue, validateInputsfunctionspure form helpers — exposed for consumers building custom action UIs against the same payload contract

Data flow

What a single rendered page does, end-to-end:

React renderer data flow: the HTTP API serves a ViewSchema; ViewSchemaConsumer dispatches it to ListView (items) or DetailView (item), each of which surfaces actions; an action button opens ActionModal, which builds its form from action.inputs and POSTs back to /actions/{fqn}.

The renderer never inspects domain types directly — every choice is made from the ViewSchema. Adding a field to your plugin shows up as a new column or a new form control on the next request, with no UI change.

Usage

Wrap your app once in AususProvider, then render a projection:

import { AususProvider, ViewSchemaConsumer } from "@ausus/renderer-react";

function App() {
return (
<AususProvider apiBaseUrl="http://localhost:8080/api" tenant="acme">
<ViewSchemaConsumer projection="billing.invoice.summary" />
</AususProvider>
);
}

ViewSchemaConsumer fetches the ViewSchema, then:

  • renders ListView if the schema's data.items is present;
  • renders DetailView if data.item is present (a subject prop is required);
  • shows a loading state while fetching and an error state with a retry button on failure.

The provider

<AususProvider
apiBaseUrl="http://localhost:8080/api"
tenant="acme"
fetcher={customFetch} // optional — defaults to window.fetch
/>

The optional fetcher lets you inject auth headers, retries, or a test double. It is the seam where you add the authentication the backend does not provide.

Hooks directly

const { schema, loading, error, refetch } = useViewSchema("billing.invoice.summary");
const { invoke, pending, lastError } = useAction("billing.invoice.issue");

await invoke({ subject: ref, inputs: {} });

useAction always awaits the server — there is no optimistic UI in v0.1.0.

Action forms

ActionModal builds its form entirely from action.inputs — the ActionDescriptor.inputs array the runtime emits. There is no entity-specific code in the renderer: pointing it at any action that declares inputs renders the matching form.

typeControlSubmitted shape
string<input type="text"> (honors typeOptions.maxLength)string
integer<input type="number" step="1">number (truncated)
money<input type="number" step="0.01"> + fixed currency label{ amount, currency }
enum<select> populated from typeOptions.optionsstring
boolean<input type="checkbox">boolean
datetime<input type="datetime-local">string
unknowntext input fallbackstring

Required inputs (required: true in the descriptor, i.e. no default and not nullable) are marked with a * indicator and validated before submit. A failed required-field check shows an inline ausus-input-error next to the control and blocks submission; a server-side failure populates the modal's top-level error block.

Actions whose inputs are empty (transitions like issue / cancel) skip the form and show the confirmation prompt instead, matching the existing behavior.

Update actions and prefill

When an ActionDescriptor carries initialValues (v0.2, see Action::update(...)), ActionModal treats the modal as an update form:

  • Form state is seeded from initialValues[name] via the initialFor helper (compound money flattens to its amount string for the input box; the submit reconstitutes the tuple).
  • The submit handler builds a diff payload: only inputs whose shaped current value differs from initialValues are sent. Unchanged keys are omitted, so the wire request is exactly the patch the user typed — matching the partial-PATCH semantics enforced server-side by UpdateEffect.
  • Required, nullable, default and typeOptions continue to come from the same FieldDescriptor shape; nothing changes for the per-input control itself.
// The renderer makes no distinction at the component level — same modal,
// same inputs, same submit hook. The presence of initialValues alone
// switches the payload-building strategy.
<ActionModal action={renameDescriptor} subject={issueRef} onClose={...} />

The exported helpers — inputDefault, isRequired, shapeValue, validateInputs plus the v0.2 additions initialFor, isUnchanged, buildCreatePayload, buildUpdatePayload — are the same pure functions ActionModal uses internally. A consumer building a custom form widget can reuse them to stay on the runtime's payload contract without re-implementing the type-shaping rules.

Styling

The renderer emits semantic class names (ausus-table, ausus-badge, ausus-modal, ausus-btn, …) but ships no CSS file. You provide the stylesheet. The class names are stable and documented by their use in the components.

Current v0.1.0 limitations

  • No bundled CSS — you supply styling for the ausus-* class names.
  • No routerViewSchemaConsumer renders one projection; wiring list → detail navigation is the host application's job.
  • No optimistic UI — every action awaits the server response.
  • ActionModal exposes one control per field type — string / integer / money / enum / boolean / datetime. Richer editors (rich text, file uploads, related-record pickers, validation rules beyond required) are not in v0.1.0.
  • WorkflowBadge colours are a fixed palette keyed on common state names (DRAFT, ISSUED, PAID, CANCELLED); other states get a default colour.
  • The workflow-state field is detected by a heuristic (an enum field named status).

Renderer v0.2.0-alpha.4 — peerSchemaVersion contract

Starting at @ausus/renderer-react@0.2.0-alpha.4, the package declares a peerSchemaVersion field at the top of its package.json:

{
"peerSchemaVersion": "^1.0.0"
}

This formalises the renderer's ViewSchema compatibility window. A backend release whose emitted schemaVersion satisfies this semver range is consumable by the renderer. The current backend (v0.2.0-alpha.4) emits schemaVersion: "1.0.0", which satisfies ^1.0.0.

Compatibility rules

  • A backend release that does NOT change schemaVersion does NOT require a renderer release.
  • A renderer release that adds new optional widgets/props does NOT require a backend bump.
  • A schemaVersion major bump (e.g., 1.x2.x) requires a synchronised renderer release expanding peerSchemaVersion to include the new range.

The contract is enforced by scripts/check-renderer-alignment.sh (CI step in release-gate.yml). PRs that desynchronize the renderer and the backend wire format fail the release gate.

npm dist-tag policy

The renderer is published to npm with this dist-tag rule:

  • Pre-release tags (-alpha.*, -beta.*, -rc.*) publish to @next.
  • Until a stable 1.x.x exists on npm, the latest pre-release is also promoted to @latest, so npm install @ausus/renderer-react (default dist-tag) returns something installable.
  • Once a stable 1.x.x is published, @latest moves to it and stays there; subsequent pre-releases only update @next.

Practical install commands during alpha:

# Default — gets the current alpha (because @latest is still the alpha)
npm install @ausus/renderer-react

# Explicit alpha
npm install @ausus/renderer-react@next