Skip to content

@umpire/store

@umpire/store is the generic foundation for store-backed adapters. It expects a strict store contract:

  • getState() returns the full store snapshot
  • subscribe((next, prev) => unsubscribe) provides both current and previous state

That strictness is intentional. It keeps transition-aware foul detection in one place and lets thin ecosystem packages normalize into the same contract.

Terminal window
yarn add @umpire/core @umpire/store
function fromStore<
S,
F extends Record<string, FieldDef>,
C extends Record<string, unknown> = Record<string, unknown>,
>(
ump: Umpire<F, C>,
store: {
getState(): S
subscribe(listener: (state: S, prevState: S) => void): () => void
},
options: {
select: (state: S) => InputValues<F>
conditions?: (state: S) => C
},
): UmpireStore<F>
interface UmpireStore<F extends Record<string, FieldDef>> {
field(name: keyof F & string): FieldStatus
get fouls(): Foul<F>[]
getAvailability(): AvailabilityMap<F>
subscribe(listener: (availability: AvailabilityMap<F>) => void): () => void
destroy(): void
}

select maps your store state to the flat { [fieldName]: value } shape Umpire expects. conditions carries external context rules can read but that isn’t a field itself (plan tier, user role, feature flags). Neither has to come from the same slice.

See Selection for the full breakdown of patterns.

  • @umpire/zustand is a zero-shim re-export because Zustand already satisfies (next, prev).
  • @umpire/redux tracks previous state internally and delegates here.
  • @umpire/pinia snapshots previous $state and delegates here.
  • @umpire/tanstack-store snapshots previous .state and delegates here.
  • @umpire/vuex snapshots previous state and delegates here.

Signal-first libraries like Jotai, Valtio, MobX, and Preact signals are not covered by this package. They do not expose the same getState()/subscribe() surface, so they belong in a different bridge layer alongside @umpire/signals.