Add rule enforcement to a React form
Fetch the client manifest, evaluate on every change with useRuleEvaluation, draw field state, computed values and findings, and send resolutions with the submit.
The form ends up with no rule logic of its own. It builds a request from its state, evaluates it against the client manifest, and draws what comes back. The server evaluates again on submit and has the final word.
Before you start
- A backend that serves the client manifest at
GET <base>/rulesets/{id}/manifest?channel=clientwith an ETag: the rule server does, and enforcement guide step 5 shows it in Spring Boot. Never serve the bundle or the server manifest to a browser. - React 18 or later.
Steps
Install the runtime
# in a checkout of rule-cascade
npm ci && npm run build
# in your app
npm install /path/to/rule-cascade/packages/typescriptFetch and cache the manifest
import { createManifestClient, type Manifest } from '@yarlisaisolutions/rule-cascade';
const manifests = createManifestClient({ baseUrl: '/api' });
const [manifest, setManifest] = useState<Manifest>();
useEffect(() => {
void manifests.get('acme.payments.transfer').then(setManifest); // revalidated with the ETag
}, []);Build the request and evaluate on every render
Convert the form state to the entity's shape first: an empty input is null, not "", and a
number is a number.
import { useRuleEvaluation } from '@yarlisaisolutions/rule-cascade/react';
const { allowed, states, computed, findingsFor } = useRuleEvaluation(manifest, {
entity: 'Transfer',
operation: 'create',
data: toEntity(draft),
actor: user, // { id, roles } the session knows; the server uses its own
ctx: { now }, // the browser clock, read once when the screen opens
resolutions,
});If the manifest lists custom operators under operators, pass them as the third argument, the same
object on every render.
Apply field state and computed values
const swift = states['/beneficiary/swiftCode'] ?? {}; // hidden unless a rule shows it
{swift.visible && (
<input aria-label="SWIFT / BIC" required={swift.required} readOnly={swift.readOnly}
value={draft.beneficiary.swiftCode ?? ''} onChange={onSwiftChange} />
)}
{computed['/fee'] !== undefined && <p>Fee: {String(computed['/fee'])}</p>}Decide a default for every field a state rule can touch: an effect exists only while its rule applies. Display computed values; do not let the user edit them.
Draw findings next to their fields
{findingsFor('/amount').map((f) => (
<p key={f.rule} className={`finding-${f.severity}`} role={f.blocking ? 'alert' : 'status'}>
{f.message}
</p>
))}A finding whose fields are not on the screen goes in a summary, at the place its location
names. A RULE-EVALUATION-ERROR finding goes in the summary too, with its generic message.
Collect resolutions
finding.resolution === 'acknowledge': offer a confirmation; on confirm add{ rule: f.rule, type: 'acknowledge' }toresolutions.finding.resolution === 'accept-risk': if the user holds a role inf.acceptableBy, offer a justification field and add{ rule: f.rule, type: 'accept-risk', justification }.'none': the user has to change the data.
The next evaluation reports the finding as acknowledged or accepted, and it no longer blocks.
Gate the submit, send resolutions, draw the server's answer
async function submit() {
if (!manifest || !allowed) return; // decision and blocking, never the severity
const response = await fetch('/api/transfers', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ transfer: draft, resolutions }),
});
const body = await response.json();
if (response.status === 422) setServer(body.evaluation); // draw like client findings
if (body.evaluation && body.evaluation.checksum !== manifest.checksum) {
setManifest(await manifests.get(manifest.id)); // the rules changed
}
}A 422 can carry findings of server-only rules the browser has never seen; draw them with the same
components.
Done when the form shows field state, computed values and findings without a round trip; a
second manifest fetch returns 304; an acknowledged warning and an accepted risk reach the server as
resolutions; and a 422 is drawn with the same components as a client finding.
Reference
- The complete form, with findings revealed per trigger (
load,change,blur): enforcement guide step 6. - A styled, runnable form: UI form example.
- Vue, Angular, Svelte or plain scripts: call
evaluate,fieldStates,computedValuesandfindingsFordirectly (any other UI framework).
Author and ship a rule change
From an edited ruleset to production - pull request, CI checks and compile, version bump, immutable bundle, rollout and rollback.
Enforce in a backend API
Load the bundle at start-up, evaluate every state-changing operation on the server channel, answer 422 on deny, persist, then run commands once. Java, Node.js, Go and Python.