Quick Start
This page teaches one Umpire primitive at a time. Each section shows the smallest useful rule, the result of one call, and a live demo using the real library.
yarn add @umpire/coreAdd @umpire/react when you want the snapshot-tracking hook used in the final play() example.
requires()
Section titled “requires()”Use requires() when one field should stay unavailable until another field is both filled in and still eligible. It is the simplest way to build stepwise flows without mutating the form.
const ump = umpire({ fields: { password: {}, confirmPassword: {} }, rules: [requires('confirmPassword', 'password')],})
ump.check({ password: '', confirmPassword: '' }).confirmPassword// disabled: reason = 'requires password'enabledWhen()
Section titled “enabledWhen()”Use enabledWhen() when a field depends on an external fact instead of another field value. Plan tiers, feature flags, permissions, and environment checks usually belong here.
const ump = umpire({ fields: { companyName: {} }, rules: [enabledWhen('companyName', (_v, c) => c.plan === 'business', { reason: 'business plan required', })],})
ump.check({ companyName: '' }, { plan: 'personal' }).companyName// disabled: reason = 'business plan required'fairWhen()
Section titled “fairWhen()”Use fairWhen() when a field’s current value might stop being an appropriate selection after something else changes. The field stays enabled — it just carries a value that no longer fits, and play() will recommend clearing it.
const motherboard = field<string>('motherboard')
const ump = umpire({ fields: { cpu: { required: true, isEmpty: (v) => !v }, motherboard: field<string>('motherboard').required().isEmpty((v) => !v), }, rules: [ requires('motherboard', 'cpu', { reason: 'Pick a CPU first' }), fairWhen(motherboard, (mb, values) => socketFor(mb) === socketFor(values.cpu ?? ''), { reason: 'Motherboard socket no longer matches the selected CPU', }), ],})
// After switching cpu to a different socket family:ump.check({ cpu: 'amd-r7', motherboard: 'asus-z790' }, undefined).motherboard// { enabled: true, fair: false, reason: 'Motherboard socket no longer matches the selected CPU' }
ump.play( { values: { cpu: 'intel-i7', motherboard: 'asus-z790' } }, { values: { cpu: 'amd-r7', motherboard: 'asus-z790' } },)// [{ field: 'motherboard', reason: 'Motherboard socket no longer matches the selected CPU', suggestedValue: undefined }]disables()
Section titled “disables()”Use disables() when one active field should override another. Unlike requires(), it only cares that the source is active, not whether the target would otherwise be available.
const ump = umpire({ fields: { bannerMode: {}, paperSize: {} }, rules: [disables('bannerMode', ['paperSize'], { reason: 'banner mode uses continuous feed', })],})
ump.check({ bannerMode: 'on', paperSize: 'A4' }).paperSize// disabled: reason = 'banner mode uses continuous feed'oneOf()
Section titled “oneOf()”Use oneOf() when only one branch of fields should be active at a time. It is the right fit for mutually exclusive modes like shipping methods, scheduling strategies, or pickup vs delivery.
const ump = umpire({ fields: { standardRate: {}, expressRate: {}, pickupLocation: {} }, rules: [oneOf('handling', { standard: ['standardRate'], express: ['expressRate'], pickup: ['pickupLocation'], }, { activeBranch: (_v, c) => c.handling })],})
ump.check({ standardRate: '12.00' }, { handling: 'express' }).standardRate// disabled: reason = 'conflicts with express strategy'check()
Section titled “check()”check() is a predicate factory, not a standalone rule. It lets you feed validation logic into availability rules so a field can unlock only when another field is actually valid.
const ump = umpire({ fields: { email: {}, submit: {} }, rules: [enabledWhen('submit', check('email', /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/), { reason: 'enter a valid email', })],})
ump.check({ email: 'bad', submit: undefined }).submit// disabled: reason = 'enter a valid email'anyOf()
Section titled “anyOf()”Multiple rules targeting the same field are ANDed by default. Wrap them in anyOf() when any one successful path should unlock the target instead.
const ump = umpire({ fields: { phone: {}, email: {}, submit: {} }, rules: [anyOf( enabledWhen('submit', check('phone', /^\\d{10,}$/), { reason: 'enter a valid phone' }), enabledWhen('submit', check('email', /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/), { reason: 'enter a valid email' }), )],})
ump.check({ phone: '', email: '', submit: undefined }).submit// disabled: reasons = ['enter a valid phone', 'enter a valid email']play()
Section titled “play()”check() tells you what is available now. play() tells you what just fell out of play after a transition, which is why plan switches and feature flips can surface reset recommendations even if the values did not change.
const ump = umpire({ fields: { companyName: {}, companySize: {} }, rules: [enabledWhen('companyName', (_v, c) => c.plan === 'business'), enabledWhen('companySize', (_v, c) => c.plan === 'business'), requires('companySize', 'companyName')],})
ump.play( { values: { companyName: 'Acme', companySize: '50' }, conditions: { plan: 'business' } }, { values: { companyName: 'Acme', companySize: '50' }, conditions: { plan: 'personal' } },)// fouls: companyName, companySizeWhere to next
Section titled “Where to next”- Read Availability for the mental model behind
enabled,required, and reason precedence. - Browse Rule Primitives for full helper signatures, defaults, and composition rules.
- See the Signup example and the React adapter when you want a larger end-to-end form.