Skip to main content

Store

Overview

The Store is the central state container for Reservoir. It coordinates feature states, dispatches actions through the middleware pipeline, invokes reducers to update state, notifies subscribers, and triggers effects for async operations. (IStore)

What Is the Store?

The Store implements IStore and provides five core operations:

MethodDescription
Dispatch(IAction)Sends an action through the pipeline
GetState<TState>()Retrieves current feature state
GetStateSnapshot()Returns all feature states as a dictionary
Subscribe(Action)Registers a listener for state changes
StoreEventsObservable stream for external integrations
public interface IStore : IDisposable
{
IObservable<StoreEventBase> StoreEvents { get; }

IReadOnlyDictionary<string, object> GetStateSnapshot();

void Dispatch(IAction action);

TState GetState<TState>()
where TState : class, IFeatureState;

IDisposable Subscribe(Action listener);
}

(IStore)

Registering the Store

Register the Store via AddReservoir():

services.AddReservoir();

This registers IStore as scoped, resolving all IFeatureStateRegistration and IMiddleware instances from DI:

public static IServiceCollection AddReservoir(
this IServiceCollection services
)
{
services.TryAddSingleton(TimeProvider.System);
services.TryAddScoped<IStore>(sp => new Store(
sp.GetServices<IFeatureStateRegistration>(),
sp.GetServices<IMiddleware>(),
sp.GetRequiredService<TimeProvider>()));
return services;
}

(ReservoirRegistrations.AddReservoir)

Scoped Lifetime

The Store is registered as scoped. Its lifetime follows the dependency-injection scope configured by the host.

Dispatch Pipeline

When you call store.Dispatch(action), the action flows through a well-defined pipeline:

Pipeline Steps

  1. Middleware Pipeline - Each registered middleware can inspect, modify, or short-circuit the action
  2. Reducers - All root reducers process the action and update their feature states
  3. Notify Subscribers - All registered listeners are invoked synchronously
  4. Effects - Root effects handle the action asynchronously; returned actions are dispatched
private void CoreDispatch(IAction action)
{
// Emit pre-dispatch event
storeEventSubject.OnNext(new ActionDispatchingEvent(action));

// Run reducers for feature states
ReduceFeatureStates(action);

// Emit post-dispatch event with current state snapshot
storeEventSubject.OnNext(new ActionDispatchedEvent(action, GetStateSnapshot()));

// Notify listeners of state change
NotifyListeners();

// Finally, trigger action effects asynchronously
_ = TriggerEffectsAsync(action);
}

(Store.CoreDispatch)

Dispatching Actions

Call Dispatch on the store to send actions through the pipeline. (IStore.Dispatch)

Components inheriting StoreComponent can call its protected Dispatch helper. (StoreComponent.Dispatch)

Dispatch Rules

  • Synchronous reducers and listeners - Reducers run first, then subscribers are notified
  • Effects are async - Effects are triggered asynchronously after the reducers and notifications
  • Null actions throw - Dispatch(null) throws ArgumentNullException
  • Disposed throws - Dispatching to a disposed store throws ObjectDisposedException

(Store.CoreDispatch, StoreTests.DispatchAfterDisposeThrowsObjectDisposedException, StoreTests.DispatchWithNullActionThrowsArgumentNullException)

Reading State

Use GetState<TState>() to retrieve the current value of a feature state:

private string? SelectedEntityId => GetState<EntitySelectionState>().EntityId;

For derived values, prefer selectors to encapsulate logic. The Spring sample uses selectors for all state access:

private string? SelectedEntityId => 
Select<EntitySelectionState, string?>(EntitySelectionSelectors.GetEntityId);

(Spring.Index, StoreComponent.GetState)

GetState Rules

  • Returns the current snapshot of the feature state
  • Throws InvalidOperationException if the feature state is not registered:
No feature state registered for 'entitySelection'.
Call AddFeatureState<EntitySelectionState>() during service registration.

(Store.GetState)

Subscribing to Changes

Use Subscribe to register a listener that runs after every dispatch. (IStore.Subscribe)

Subscription Behavior

  • Listeners are called synchronously after reducers complete and before effects run
  • Listeners receive no parameters-query state via GetState<TState>()
  • Dispose the returned IDisposable to unsubscribe
  • Subscriptions can be disposed multiple times safely

(Store.CoreDispatch, StoreTests.SubscriptionDisposeCanBeCalledMultipleTimes)

For an example of unsubscribe behavior, see the unit test. (StoreTests.UnsubscribedListenerDoesNotReceiveNotifications)

Blazor Integration

For Blazor components, inherit from StoreComponent instead of managing subscriptions manually:

InletComponent is a concrete example of a component that derives from StoreComponent. (InletComponent)

StoreComponent handles:

  • Automatic subscription - Subscribes to the store in OnInitialized
  • Automatic re-render - Calls StateHasChanged when state changes
  • Automatic cleanup - Disposes the subscription when the component is disposed
protected override void OnInitialized()
{
base.OnInitialized();
storeSubscription = Store.Subscribe(OnStoreChanged);
}

private void OnStoreChanged()
{
_ = InvokeAsync(StateHasChanged);
}

(StoreComponent)

Store Lifecycle

Construction

The Store can be constructed two ways:

  1. Via DI (recommended) - AddReservoir() registers the Store with feature registrations and middleware resolved from DI
  2. Manually - Pass feature registrations and middleware directly to the constructor

Manual construction is used in tests to validate middleware behavior. (Store constructor, StoreTests.ConstructorWithMiddlewareCollectionRegistersMiddleware)

Disposal

The Store implements IDisposable. When disposed:

  • All subscriptions are cleared
  • All feature states, reducers, and effects are cleared
  • Subsequent Dispatch, GetState, or Subscribe calls throw ObjectDisposedException

(StoreTests.DispatchAfterDisposeThrowsObjectDisposedException, Store.Dispose, Store.Dispose(bool))

Effect Error Handling

Effects run asynchronously after dispatch. If an effect throws:

  • The exception is swallowed to prevent breaking the dispatch pipeline
  • Other effects continue to run
  • Effects should handle their own errors by emitting error actions

(Store.TriggerEffectsAsync)

Observable Store Events

The store exposes an observable stream of events via StoreEvents, enabling external integrations to observe store activity without subclassing. DevTools uses this pattern to report actions and state to the browser extension.

StoreEvents Property

IObservable<StoreEventBase> StoreEvents { get; }

Subscribers receive events synchronously during dispatch. Keep handlers fast to avoid blocking the pipeline.

(IStore.StoreEvents)

Store Event Types

EventWhen EmittedPayload
StoreInitializedEventOnce during store constructionInitialSnapshot
ActionDispatchingEventBefore reducers runAction
ActionDispatchedEventAfter reducers, before effectsAction, StateSnapshot
StateRestoredEventAfter system action restores statePreviousSnapshot, NewSnapshot, Cause

All events inherit from StoreEventBase.

(StoreEventBase, ActionDispatchingEvent, ActionDispatchedEvent, StateRestoredEvent, StoreInitializedEvent)

Example: Subscribing to Events

store.StoreEvents.Subscribe(new ActionObserver());

private sealed class ActionObserver : IObserver<StoreEventBase>
{
public void OnNext(StoreEventBase evt)
{
if (evt is ActionDispatchedEvent dispatched)
{
Console.WriteLine($"Action: {dispatched.Action.GetType().Name}");
}
}

public void OnCompleted() { }
public void OnError(Exception error) { }
}

System Actions

System actions implement ISystemAction and are handled directly by the store rather than by user-defined reducers. They enable external components (like DevTools) to command the store through the standard dispatch mechanism, maintaining unidirectional data flow.

ISystemAction

public interface ISystemAction : IAction { }

System actions do not trigger user reducers or effects. They are processed internally and emit StateRestoredEvent to notify observers.

(ISystemAction)

Built-in System Actions

ActionPurposeParameters
RestoreStateActionRestore state from a snapshotSnapshot, NotifyListeners (default: true)
ResetToInitialStateActionReset to initial stateNotifyListeners (default: true)

(RestoreStateAction, ResetToInitialStateAction)

Example: Time-Travel

// Save a snapshot
var snapshot = store.GetStateSnapshot();

// ... user performs actions ...

// Restore to saved snapshot
store.Dispatch(new RestoreStateAction(snapshot));

// Or reset to initial state
store.Dispatch(new ResetToInitialStateAction());

GetStateSnapshot

Returns a dictionary of all current feature states keyed by feature key:

IReadOnlyDictionary<string, object> GetStateSnapshot();

Use this for:

  • Saving state for later restoration
  • Reporting current state to external tools
  • Debugging and logging

(IStore.GetStateSnapshot)

Store Internals

For advanced scenarios, understanding the Store's internal structure helps:

FieldTypePurpose
featureStatesConcurrentDictionary<string, object>Maps FeatureKey → current state
rootReducersConcurrentDictionary<string, object>Maps FeatureKey → IRootReducer<TState>
rootActionEffectsConcurrentDictionary<string, object>Maps FeatureKey → IRootActionEffect<TState>
middlewaresList<IMiddleware>Ordered middleware pipeline
listenersList<Action>Registered subscribers
storeEventSubjectStoreEventSubject<StoreEventBase>Observable subject for store events

(Store fields)

Middleware Pipeline Building

Middleware wraps around the core dispatch in reverse registration order (last registered wraps first):

private Action<IAction> BuildMiddlewarePipeline(Action<IAction> coreDispatch)
{
Action<IAction> next = coreDispatch;

// Build pipeline in reverse order (last middleware wraps first)
for (int i = middlewares.Count - 1; i >= 0; i--)
{
IMiddleware middleware = middlewares[i];
Action<IAction> currentNext = next;
next = action => middleware.Invoke(action, currentNext);
}

return next;
}

(Store.BuildMiddlewarePipeline)

Summary

ConceptDescription
StoreCentral state container implementing IStore
DispatchSends actions through middleware → reducers → notify → effects
GetStateReturns current feature state snapshot
GetStateSnapshotReturns all feature states as a dictionary
StoreEventsObservable stream for external integrations (DevTools, logging)
System ActionsISystemAction for store-internal operations (time-travel)
SubscribeRegisters listener called after every dispatch
LifetimeScoped (per DI scope)
DisposalClears all state and subscriptions; subsequent calls throw
Error handlingEffects swallow exceptions; emit error actions instead

(IStore, Store, StoreComponent)

Next Steps

  • Reservoir Overview - Understand the dispatch pipeline end-to-end
  • DevTools - See how DevTools uses StoreEvents and system actions
  • Actions - Define what can happen in your application
  • Reducers - Update state in response to actions
  • Effects - Handle async operations and side effects
  • Middleware - Intercept and transform actions
  • Feature State - Organize state into modular slices
  • StoreComponent - Blazor base component for store integration
  • Selectors - Derive computed values from state