Configuration-Driven Design
Why a Prism form is declared as data, and what that buys you.
Declaring a form as data
A typical hand-rolled React Hook Form integration assembles a form by writing one Controller
(or register call) per field, by hand, in JSX. Prism inverts this: you declare which fields
exist and what type each one is, once, as a plain configuration object:
const definition = createFormBuilder(registry).defineForm<SignupValues>({
name: { type: 'text', label: 'Full name' },
email: { type: 'text', label: 'Email' },
age: { type: 'number', label: 'Age' },
});<PrismField form={form} name="name" /> then renders whatever defineForm configured for
name, resolved against the registry at render time. Adding a field means adding one entry to
this object, not writing a new block of JSX.
What this buys you
- One source of truth per form. The field list, its types, and its extra props live in one object, not scattered across JSX.
- Compile-time safety a hand-rolled integration can't get for free.
createFormBuilder(registry).defineForm<TValues>checks that every configured field'stypeexists in the registry, that its component's declared value type exactly matchesTValues[K], and that every required extra prop is present. Astring-valued field component cannot be configured against anumberfield, see Forms. - Static configuration, kept separate from runtime state. A
FormDefinitionis immutable once created (ADR-0013); the actual, changing form state (values, dirty flags, errors) lives entirely in React Hook Form, which Prism never duplicates or shadows.
What it deliberately doesn't do
Configuration-driven doesn't mean "no imperative escape hatch." A field's configuration is
static, fixed once in defineForm, there's no way to pass different props to a field on a
given render based on another field's current value. For genuinely dynamic cases, Prism's
answer is to drop to React Hook Form's own Controller directly, reachable via form.control
since usePrismForm returns the complete, real UseFormReturn, see
Advanced Forms Usage. This is not a gap to
be papered over with a dynamic-configuration API later, it is the architecture's "preserve full
RHF access" principle working as intended.