@umpire/react
@umpire/react is intentionally small. It does not add a subscription layer or side-effect system. It just derives check() and play() results during render.
Live component
Signup Form
No useEffect
Pass current values and conditions in. Get live availability and reset guidance back. Pure derivation on render.
Conditions
{ plan: 'personal' }requiredenabled
requiredenabled
disabled
disabled
disabled
Fill in a company name on the business plan, then switch back to personal — a foul recommends clearing the stale value.
Derived hook state
Hook Output
Hook
const { check, fouls } = useUmpire(demoUmp, values, conditions)check
{
"email": {
"enabled": true,
"fair": true,
"required": true,
"reason": null,
"reasons": []
},
"password": {
"enabled": true,
"fair": true,
"required": true,
"reason": null,
"reasons": []
},
"companyName": {
"enabled": false,
"fair": true,
"required": false,
"reason": "business plan required",
"reasons": [
"business plan required"
]
},
"confirmPassword": {
"enabled": false,
"fair": true,
"required": false,
"reason": "requires password",
"reasons": [
"requires password"
]
},
"companySize": {
"enabled": false,
"fair": true,
"required": false,
"reason": "business plan required",
"reasons": [
"business plan required",
"requires companyName"
]
}
}fouls
[]Install
Section titled “Install”yarn add @umpire/core @umpire/reactuseUmpire()
Section titled “useUmpire()”import { useUmpire } from '@umpire/react'
function useUmpire< F extends Record<string, FieldDef>, C extends Record<string, unknown>,>( ump: Umpire<F, C>, values: InputValues, conditions?: C,): { check: AvailabilityMap<F> fouls: Foul<F>[]}Behavior
Section titled “Behavior”checkis memoized fromump.check(values, conditions, prevValues).foulsis memoized fromump.play(previousSnapshot, currentSnapshot).- Previous values are tracked internally with
useRef. - There is no
useEffect().
That means the hook is pure derivation. You decide what to do with fouls in event handlers or higher-level state logic.
Example
Section titled “Example”import { useState } from 'react'import { useUmpire } from '@umpire/react'import { enabledWhen, requires, umpire } from '@umpire/core'
const fields = { email: { required: true, isEmpty: (v: unknown) => !v }, password: { required: true, isEmpty: (v: unknown) => !v }, confirmPassword: { required: true, isEmpty: (v: unknown) => !v }, companyName: { isEmpty: (v: unknown) => !v }, companySize: { isEmpty: (v: unknown) => !v },}
// Conditions represent external facts — not user input.// The plan tier comes from account state, so it's a condition.type SignupConditions = { plan: 'personal' | 'business' }
const signupUmp = umpire<typeof fields, SignupConditions>({ fields, rules: [ // confirmPassword is only available once password is satisfied (non-empty). requires('confirmPassword', 'password'),
// Company fields gate on the plan condition. enabledWhen('companyName', (_v, cond) => cond.plan === 'business', { reason: 'business plan required', }), enabledWhen('companySize', (_v, cond) => cond.plan === 'business', { reason: 'business plan required', }),
// Transitive chain: business plan → companyName filled → companySize available. requires('companySize', 'companyName'), ],})
export function SignupForm() { const [plan, setPlan] = useState<SignupConditions['plan']>('personal') const [values, setValues] = useState(() => signupUmp.init()) const { check, fouls } = useUmpire(signupUmp, values, { plan })
// fouls recommend clearing stale values when fields become disabled. function applyResets() { setValues((current) => { const next = { ...current } for (const foul of fouls) { next[foul.field] = foul.suggestedValue } return next }) }
return ( <form> <select value={plan} onChange={(e) => setPlan(e.currentTarget.value as SignupConditions['plan'])} > <option value="personal">Personal</option> <option value="business">Business</option> </select>
<input placeholder="Password" value={String(values.password ?? '')} onChange={(e) => setValues((cur) => ({ ...cur, password: e.currentTarget.value })) } />
{check.confirmPassword.enabled && ( <input placeholder="Confirm password" value={String(values.confirmPassword ?? '')} onChange={(e) => setValues((cur) => ({ ...cur, confirmPassword: e.currentTarget.value })) } /> )}
{check.companyName.enabled && ( <input placeholder="Company name" value={String(values.companyName ?? '')} onChange={(e) => setValues((cur) => ({ ...cur, companyName: e.currentTarget.value })) } /> )}
{!check.companyName.enabled && <p>{check.companyName.reason}</p>}
{fouls.length > 0 && ( <div> <p>Stale values detected:</p> <pre>{JSON.stringify(fouls, null, 2)}</pre> <button type="button" onClick={applyResets}>Apply resets</button> </div> )} </form> )}- The hook does not apply recommendations automatically — you decide when and how to act on fouls.
- Passing stable
valuesandconditionsobjects keeps memoization effective. prevhandling foroneOf()is already wired in through the internal ref.