Skip to content

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.

Terminal window
yarn add @umpire/core

Add @umpire/react when you want the snapshot-tracking hook used in the final play() example.

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'
Password
enabled
Confirm password
disabled
requires password

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'
Company name
disabled
business plan required

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 }]

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'
Banner mode
idle
Paper size
enabled

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'
Standard rate
enabled
Express rate
disabled
conflicts with standard strategy
Pickup location
disabled
conflicts with standard strategy

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'
Email
enabled
Submit
disabled
enter a valid email

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']
Phone
enabled
Email
enabled
Submit
disabled
enter a valid phone or enter a valid email

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, companySize