Skip to content

@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

useUmpire()
No useEffect

Pass current values and conditions in. Get live availability and reset guidance back. Pure derivation on render.

Conditions{ plan: 'personal' }
requiredenabled
check.emailavailable
requiredenabled
check.passwordavailable
disabled
check.confirmPasswordrequires password
disabled
check.companyNamebusiness plan required
disabled
check.companySizebusiness plan required

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

live JSON
Hookconst { check, fouls } = useUmpire(demoUmp, values, conditions)
checkAvailabilityMap
{
  "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[]
[]
Terminal window
yarn add @umpire/core @umpire/react
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>[]
}
  • check is memoized from ump.check(values, conditions, prevValues).
  • fouls is memoized from ump.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.

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 values and conditions objects keeps memoization effective.
  • prev handling for oneOf() is already wired in through the internal ref.