Calendar Recurrence
April 2026
Calendar recurrence
Adding a date here activates the disables() rule — all pattern fields go dark and play() will recommend clearing any active values.
oneOf active — weekday strategy is locked in. Day-of-month is unavailable until weekdays are cleared.
A recurring event scheduler where fields interact in multiple ways at once. This is the kind of config panel that turns into a mess of useEffect chains without a system to manage it.
The Problem
Section titled “The Problem”A calendar app lets users define how events repeat. The options are interdependent — and none of this is a “form” in the traditional sense:
- Explicit dates override all pattern-based recurrence (if you pick specific dates, weekday/monthly patterns don’t apply)
- Two scheduling strategies are mutually exclusive: pick specific hours, or define a start/end time interval — not both
- Interval fields depend on each other: you can’t set a repeat interval or end time without a start time
- Exclusions only make sense when a recurrence pattern exists
- “Fixed between” bounds only make sense when both a start and end date are set
The Rules
Section titled “The Rules”import { disables, enabledWhen, oneOf, requires, umpire } from '@umpire/core'
const scheduler = umpire({ fields: { // Date bounds fromDate: {}, toDate: {}, fixedBetween: { default: false },
// Explicit dates (overrides patterns) dates: {},
// Day-level recurrence patterns everyWeekday: {}, everyDate: {}, everyMonth: {},
// Exclusions from patterns exceptDates: {}, exceptBetween: {},
// Sub-day strategy A: specific hours everyHour: {},
// Sub-day strategy B: time interval startTime: {}, endTime: {}, repeatEvery: {},
// Shared duration: {}, }, rules: [ // Explicit dates shut down everything pattern-based disables('dates', [ 'everyWeekday', 'everyDate', 'everyMonth', 'everyHour', 'startTime', 'endTime', 'repeatEvery', 'exceptDates', 'exceptBetween', ]),
// Pick one: specific hours OR a time interval oneOf('subDayStrategy', { hourList: ['everyHour'], interval: ['startTime', 'endTime', 'repeatEvery'], }),
// Interval fields chain off startTime requires('repeatEvery', 'startTime'), requires('endTime', 'startTime'),
// Bounds toggle only meaningful when both dates exist enabledWhen('fixedBetween', ({ fromDate, toDate }) => !!fromDate && !!toDate),
// Exclusions only meaningful when patterns exist enabledWhen('exceptDates', (v) => !!(v.everyWeekday || v.everyDate || v.everyMonth)), enabledWhen('exceptBetween', (v) => !!(v.everyWeekday || v.everyDate || v.everyMonth)), ],})Seven rules. Three different rule types working together. The declarations read as plain English.
Step 1 — User Picks Explicit Dates
Section titled “Step 1 — User Picks Explicit Dates”let values = scheduler.init()values = { ...values, dates: ['2026-04-01', '2026-04-05'] }
const result = scheduler.check(values)Everything pattern-based shuts down:
| Field | enabled | reason |
|---|---|---|
| everyWeekday | false | 'overridden by dates' |
| everyHour | false | 'overridden by dates' |
| startTime | false | 'overridden by dates' |
| exceptDates | false | 'overridden by dates' |
One disables rule knocked out 9 fields. The reason string makes it obvious why.
Step 2 — Switch to Weekly Recurrence
Section titled “Step 2 — Switch to Weekly Recurrence”User clears explicit dates and sets up a Monday/Wednesday/Friday pattern:
const prev = valuesvalues = { ...values, dates: undefined, everyWeekday: [1, 3, 5] }
const result = scheduler.check(values)The field opens back up:
- All pattern fields are available again
- Both sub-day strategies are available (no branch chosen yet)
exceptDatesandexceptBetweenare now enabled — a recurrence pattern exists to exclude from
Step 3 — Pick the Interval Strategy
Section titled “Step 3 — Pick the Interval Strategy”User sets a start time. This activates the interval branch of oneOf:
const prev2 = valuesvalues = { ...values, startTime: '09:00' }
const result = scheduler.check(values, undefined, prev2)| Field | enabled | reason |
|---|---|---|
| everyHour | false | 'conflicts with interval strategy' |
| startTime | true | null |
| endTime | true | null |
| repeatEvery | true | null |
Three things happened from one value change:
oneOfdetected that theintervalbranch is active (startTime has a value), disablingeveryHourrequires('endTime', 'startTime')is now satisfied —endTimebecomes availablerequires('repeatEvery', 'startTime')is also satisfied
The prev parameter lets oneOf resolve ambiguity — it knows the interval branch is the newly active one.
Step 4 — Reset Recommendations
Section titled “Step 4 — Reset Recommendations”If everyHour was empty, there’s nothing to clean up:
scheduler.play({ values: prev2 }, { values })// → []But if the user had previously set specific hours before switching strategies:
scheduler.play( { values: { ...prev2, everyHour: [9, 17] } }, { values },)// → [{ field: 'everyHour',// reason: 'conflicts with interval strategy',// suggestedValue: undefined }]play() only recommends clearing fields that (1) just became disabled, (2) hold a non-empty value, and (3) don’t already match their default. No false positives.
Why This Works
Section titled “Why This Works”Three interaction patterns layered on top of each other:
disables— explicit dates are authoritative. A stale value indatesstill shuts everything down, because Umpire checks the value, not the field’s availability. The consumer has to cleardatesto release the pattern fields.oneOf— the two scheduling strategies are mutually exclusive. Noif/elsechain needed — declare the branches and Umpire handles the rest.requires— availability flows through the dependency chain.startTime→endTimeandstartTime→repeatEverypropagate naturally.
Seven declarative rules replace what would otherwise be a tangled web of useEffect hooks and conditional renders. Same idea works for any config panel, booking widget, or scheduler — not just forms.