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:
| Method | Description |
|---|---|
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 |
StoreEvents | Observable 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)
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
- Middleware Pipeline - Each registered middleware can inspect, modify, or short-circuit the action
- Reducers - All root reducers process the action and update their feature states
- Notify Subscribers - All registered listeners are invoked synchronously
- 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);
}
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)throwsArgumentNullException - 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
InvalidOperationExceptionif the feature state is not registered:
No feature state registered for 'entitySelection'.
Call AddFeatureState<EntitySelectionState>() during service registration.
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
IDisposableto 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
StateHasChangedwhen 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);
}
Store Lifecycle
Construction
The Store can be constructed two ways:
- Via DI (recommended) -
AddReservoir()registers the Store with feature registrations and middleware resolved from DI - 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, orSubscribecalls throwObjectDisposedException
(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
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.
Store Event Types
| Event | When Emitted | Payload |
|---|---|---|
StoreInitializedEvent | Once during store construction | InitialSnapshot |
ActionDispatchingEvent | Before reducers run | Action |
ActionDispatchedEvent | After reducers, before effects | Action, StateSnapshot |
StateRestoredEvent | After system action restores state | PreviousSnapshot, 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.
Built-in System Actions
| Action | Purpose | Parameters |
|---|---|---|
RestoreStateAction | Restore state from a snapshot | Snapshot, NotifyListeners (default: true) |
ResetToInitialStateAction | Reset to initial state | NotifyListeners (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
Store Internals
For advanced scenarios, understanding the Store's internal structure helps:
| Field | Type | Purpose |
|---|---|---|
featureStates | ConcurrentDictionary<string, object> | Maps FeatureKey → current state |
rootReducers | ConcurrentDictionary<string, object> | Maps FeatureKey → IRootReducer<TState> |
rootActionEffects | ConcurrentDictionary<string, object> | Maps FeatureKey → IRootActionEffect<TState> |
middlewares | List<IMiddleware> | Ordered middleware pipeline |
listeners | List<Action> | Registered subscribers |
storeEventSubject | StoreEventSubject<StoreEventBase> | Observable subject for store events |
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
| Concept | Description |
|---|---|
| Store | Central state container implementing IStore |
| Dispatch | Sends actions through middleware → reducers → notify → effects |
| GetState | Returns current feature state snapshot |
| GetStateSnapshot | Returns all feature states as a dictionary |
| StoreEvents | Observable stream for external integrations (DevTools, logging) |
| System Actions | ISystemAction for store-internal operations (time-travel) |
| Subscribe | Registers listener called after every dispatch |
| Lifetime | Scoped (per DI scope) |
| Disposal | Clears all state and subscriptions; subsequent calls throw |
| Error handling | Effects 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