Split-State Account Settings
This example is about ownership boundaries, not fancy form plumbing.
Three sections own different parts of the same account settings store:
ProfileSectionownsemailanddisplayNamePlanSectionownsplanTeamSectionownsteamSizeandteamDomain
Umpire still evaluates cross-cutting rules once, globally:
teamSizeonly enables whenplan === 'team'teamDomainrequiresteamSize > 0emailstays required the whole time
Split ownership
Account Settings
Each section owns its own slice. `fromStore()` pulls them back together with one `select()` call.
ProfileSection
Owns profile fields
state.profilePlanSection
Owns billing plan
state.billingThis section never touches team inputs directly. It only flips plan and Umpire handles the cross-section availability.
TeamSection
Owns team fields
state.teamGlobal reads
Aggregated Output
Condition
{ plan: 'personal' }Anywhere read
field('teamSize') => disabledRule 1`teamSize` only enables on the team plan.
Rule 2`teamDomain` requires `teamSize > 0`.
Rule 3`email` stays required regardless of which section owns it.
select(state)
{
"email": "alex@example.com",
"displayName": "Alex Rivera",
"teamSize": "5",
"teamDomain": "stadiumops.dev"
}availability
{
"email": {
"enabled": true,
"fair": true,
"required": true,
"reason": null,
"reasons": []
},
"displayName": {
"enabled": true,
"fair": true,
"required": false,
"reason": null,
"reasons": []
},
"teamSize": {
"enabled": false,
"fair": true,
"required": false,
"reason": "team plan required",
"reasons": [
"team plan required"
]
},
"teamDomain": {
"enabled": true,
"fair": true,
"required": false,
"reason": null,
"reasons": []
}
}The Pattern
Section titled “The Pattern”select() is the whole trick:
const umpStore = fromStore(accountUmp, store, { select: (state) => ({ email: state.profile.email, displayName: state.profile.displayName, teamSize: state.team.size, teamDomain: state.team.domain, }), conditions: (state) => ({ plan: state.billing.plan, }),})Each section keeps owning its own slice. Umpire never needs to know that the values came from different components.
What To Notice
Section titled “What To Notice”- One
fromStore()call sets up the whole app-wide availability layer. requires()still propagates transitively, so disablingteamSizealso knocks outteamDomain.- Any component can read
umpStore.field('teamSize')without mounting its ownuseUmpire()instance. - When the plan flips back to personal, fouls recommend clearing stale team values instead of mutating state automatically.
Which Package To Use
Section titled “Which Package To Use”- Use
@umpire/storeif your store already provides(next, prev). - Use
@umpire/zustandif you want the named Zustand entry point. - Use
@umpire/redux,@umpire/pinia,@umpire/tanstack-store, or@umpire/vuexwhen those libraries need the thin normalization layer.