Address Form
What this demonstrates
Section titled “What this demonstrates”A multi-country address form with 6 fields and 5 rules spanning 3 rule types. Country selection gates which regional fields appear, postal code format validation switches by country, and stale values are automatically cleaned up when their field disappears.
Rule types in play
Section titled “Rule types in play”| Rule | What it does here |
|---|---|
enabledWhen | state appears only when country is US. province appears only when country is CA. |
requires | Both state and province require country to be selected — no selecting a region without first picking a country. |
fairWhenRead | postalCode format validation switches based on country, and the read dependency on country feeds the graph so validators re-run when country changes. |
Things to try
Section titled “Things to try”- Select “United States” — State field appears. Pick a state, fill in a zip.
- Switch to “Canada” — State disappears, Province appears. The stale state value becomes a foul and is automatically cleared (strike-on-transition).
- Enter an invalid postal code — the inline error message changes based on country. Try “12345” for Canada — it fails. Try “K1A 0B1” for Canada — it passes.
- Switch countries without selecting a region — see how state and province remain disabled until country is chosen.
- Clear the country — state and province disable, and any filled stale values become fouls.
Imperative vs Declarative
Section titled “Imperative vs Declarative”Without Umpire, this form would need hand-written logic for country-change effects, conditional rendering gating, cross-field validation wiring, and stale-value cleanup — spread across event handlers, useEffect, and component state:
// Without Umpire — imperative orchestrationfunction AddressForm() { const [country, setCountry] = useState('') const [state, setState] = useState('') const [province, setProvince] = useState('') const [postalError, setPostalError] = useState('')
// Manual conditional rendering const showState = country === 'US' const showProvince = country === 'CA'
// Manual stale-value cleanup useEffect(() => { if (!showState && state) setState('') }, [showState]) useEffect(() => { if (!showProvince && province) setProvince('') }, [showProvince])
// Manual postal code validation const validatePostal = (code: string) => { if (!code) return setPostalError('') switch (country) { case 'US': ... case 'CA': ... case 'UK': ... } } // ... and so on}With Umpire, all of this is 5 declarative rules:
import { createReads, fairWhenRead } from '@umpire/reads'
function postalCodeMatchesCountry(code: string, country: unknown) { switch (country) { case 'US': return /^\d{5}(-\d{4})?$/.test(code) case 'CA': return /^[A-Z]\d[A-Z]\s?\d[A-Z]\d$/i.test(code) case 'UK': return /^[A-Z]{1,2}\d[A-Z\d]?\s?\d[A-Z]{2}$/i.test(code) default: return true }}
const addressReads = createReads({ postalCodeFair: ({ input }) => { const code = String(input.postalCode ?? '').trim() return !code || postalCodeMatchesCountry(code, input.country) },})
const ump = umpire({ fields: { street: { required: true, isEmpty: (v) => !v }, city: { required: true, isEmpty: (v) => !v }, country: { required: true, isEmpty: (v) => !v }, state: { required: true, isEmpty: (v) => !v }, province: { required: true, isEmpty: (v) => !v }, postalCode: { required: true, isEmpty: (v) => !v }, }, rules: [ enabledWhen('state', (v) => v.country === 'US', { reason: 'Only US addresses use states', }), enabledWhen('province', (v) => v.country === 'CA', { reason: 'Only Canadian addresses use provinces', }), requires('state', 'country'), requires('province', 'country'), fairWhenRead('postalCode', 'postalCodeFair', addressReads, { reason: 'Invalid format for selected country', }), ],})No useEffect, no manual cleanup, no hand-wired validation wiring. Umpire derives availability, fouls, and validation from the rules alone.
Implementation
Section titled “Implementation”This demo uses @umpire/tanstack-form with TanStack Form and React. Each field component uses TanStack Form’s <form.Field> with per-field validators auto-wired by umpireFieldValidator — Umpire reads its own graph to determine which fields need to revalidate when an upstream changes.
The useUmpireForm hook subscribes to the form store, derives availability and fouls, and applies strike-on-transition automatically. This demo passes that derived surface into small field components, which render null when ump.field(name).enabled is false. You can also gate fields at the parent level with ump.field('state').enabled; it is the same availability signal, so choose the composition style that reads best in your app.