Skip to main content

Redux DevTools Integration

Overview

Reservoir provides opt-in integration with the Redux DevTools browser extension. When enabled, a background service observes all dispatched actions and state changes, reporting them to DevTools for time-travel debugging, state inspection, and action replay.

This page covers the registration requirements, configuration options, enablement modes, composition architecture, and strict time-travel rehydration feature.

Minimum Setup

1. Register DevTools

Register DevTools after AddReservoir() in your Program.cs:

IReservoirBuilder reservoir = builder.AddReservoir();
reservoir.AddReservoirDevTools(options =>
{
options.Enablement = ReservoirDevToolsEnablement.DevelopmentOnly;
});

(ReservoirDevToolsRegistrations.AddReservoirDevTools)

2. Add the Initializer Component

Add ReservoirDevToolsInitializerComponent to your App.razor:

@using Mississippi.Reservoir.Client

<ReservoirDevToolsInitializerComponent/>

<Router AppAssembly="@typeof(App).Assembly">
<!-- ... -->
</Router>
important

This step is required. The component calls Initialize() after the Blazor rendering context is available. Without it, DevTools will not connect.

(ReservoirDevToolsInitializerComponent)

3. Install the Browser Extension

Install the Redux DevTools extension for your browser:

4. Run Your Application

Open DevTools in your browser and navigate to the Redux tab. You will see actions and state as they are dispatched.

Enablement Modes

DevTools integration is disabled by default. Use the Enablement property to control when integration is active.

ReservoirDevToolsEnablement Values

public enum ReservoirDevToolsEnablement
{
Off = 0, // DevTools integration is disabled (default)
DevelopmentOnly = 1, // Enabled only when IHostEnvironment.IsDevelopment() returns true
Always = 2, // Enabled in all environments
}
ValueBehavior
OffDevTools integration is disabled. No JavaScript interop occurs. This is the default.
DevelopmentOnlyDevTools integration is enabled only when IHostEnvironment.IsDevelopment() returns true. Recommended for most applications.
AlwaysDevTools integration is enabled in all environments including production. Use with caution.

(ReservoirDevToolsEnablement)

Example

reservoir.AddReservoirDevTools(options =>
{
// Only enable in development
options.Enablement = ReservoirDevToolsEnablement.DevelopmentOnly;
});
warning

Enabling DevTools in production environments exposes application state to users with browser DevTools access. Use DevelopmentOnly or Off for production deployments.

Configuration Options

The ReservoirDevToolsOptions class provides configuration for the DevTools integration:

OptionTypeDefaultDescription
EnablementReservoirDevToolsEnablementOffControls when DevTools integration is active. See Enablement Modes.
Namestring?nullInstance name shown in DevTools dropdown when multiple instances exist.
MaxAgeint?nullMaximum number of actions to retain in history. When exceeded, oldest actions are removed.
Latencyint?nullBatching latency in milliseconds. Actions dispatched within this window are batched together.
AutoPausebool?nullWhen true, pauses recording when DevTools window is not open to reduce overhead.
IsStrictStateRehydrationEnabledboolfalseWhen true, time-travel rejects payloads missing any feature state. See Strict State Rehydration.
ThrowOnMissingInitializerbool?nullControls error handling when the initializer component is missing. See Missing Component Detection.
ActionSanitizerFunc<IAction, object?>?nullTransform actions before sending. Return null to use default serialization. See Sanitizers.
StateSanitizerFunc<IReadOnlyDictionary<string, object>, object?>?nullTransform state snapshot before sending. Return null to use the original snapshot. See Sanitizers.
AdditionalOptionsIDictionary<string, object?>empty dictionaryPass through additional Redux DevTools extension options.
SerializerOptionsJsonSerializerOptionsweb defaults with case-insensitive property namesControls JSON-based state rehydration during time-travel operations.

(ReservoirDevToolsOptions)

Example Configuration

reservoir.AddReservoirDevTools(options =>
{
options.Enablement = ReservoirDevToolsEnablement.DevelopmentOnly;
options.Name = "My Blazor App";
options.MaxAge = 50;
});

How It Works

DevTools integration uses composition rather than inheritance. When enabled, AddReservoirDevTools registers ReduxDevToolsService as a scoped service that subscribes to IStore.StoreEvents. The service is initialized via ReservoirDevToolsInitializerComponent, which calls Initialize() after the Blazor rendering context is available. This approach keeps the store implementation unchanged while allowing external integrations to observe its activity.

Scoped Service Lifetime

ReduxDevToolsService is registered as a scoped service to match the lifetime of IStore. In Blazor WebAssembly, scoped services are effectively singletons (one scope for the application lifetime). In Blazor Server, each circuit gets its own scope with its own store and DevTools instance.

Initialization via Component

The ReservoirDevToolsInitializerComponent is a renderless Blazor component that:

  1. Calls Initialize() on first render (when JS interop is available)
  2. Calls Stop() on disposal to unsubscribe from store events

This design ensures DevTools connects only after the Blazor rendering context is ready, avoiding JavaScript interop errors during server prerendering.

Connection Logging

When DevTools connects successfully, ReduxDevToolsService logs at Information level:

Redux DevTools connected successfully.

If connection fails (e.g., the browser extension is not installed), a Warning is logged:

Failed to connect to Redux DevTools. Ensure the Redux DevTools browser extension is installed and the page is open in a supported browser.

(DevToolsLoggerExtensions)

Missing Component Detection

Reservoir includes a hosted service that detects when DevTools is enabled but the initializer component is missing. This prevents silent failures where DevTools appears to be configured but never connects.

How Detection Works

After startup, DevToolsInitializationCheckerService waits 5 seconds and checks whether Initialize() was called. If DevTools is enabled (not Off) and initialization has not occurred, the service either throws an exception or logs a warning depending on configuration.

ThrowOnMissingInitializer Behavior

ValueBehavior
null (default)Throws in Development environments; logs a warning in Production.
trueAlways throws an InvalidOperationException.
falseAlways logs a warning without throwing.

Configuration Example

reservoir.AddReservoirDevTools(options =>
{
options.Enablement = ReservoirDevToolsEnablement.DevelopmentOnly;

// Explicitly control missing-component behavior
options.ThrowOnMissingInitializer = true; // Always throw
});
tip

Leave ThrowOnMissingInitializer at its default (null) to get fail-fast behavior during development while avoiding runtime exceptions in production.

(DevToolsInitializationCheckerService)

Composition Pattern

  1. Store emits events: The store publishes StoreEventBase events through IStore.StoreEvents during dispatch
  2. DevTools observes: ReduxDevToolsService subscribes to the event stream and reports actions/state to the browser extension
  3. Time-travel via system actions: Commands from DevTools (jump, reset, rollback) are translated into system actions dispatched to the store

This design maintains unidirectional data flow-even time-travel commands go through Dispatch().

(ReduxDevToolsService)

Time-Travel Debugging

Redux DevTools supports time-travel debugging, allowing you to jump to previous states, reset, rollback, and import/export state. When you perform these operations in DevTools, the ReduxDevToolsService translates them into system actions dispatched to the store.

Supported Operations

OperationSystem ActionDescription
Jump to StateRestoreStateActionNavigate to a specific point in action history
ResetResetToInitialStateActionReturn to the initial state registered during startup
RollbackRestoreStateActionReturn to the last committed state snapshot
Commit(internal)Mark current state as the new baseline for rollback
ImportRestoreStateActionLoad state from an exported JSON file
note

System actions are handled directly by the store and do not trigger user-defined reducers or effects. They emit StateRestoredEvent to notify observers.

Strict State Rehydration

By default, time-travel operations use best-effort rehydration: if a feature state is missing from the incoming payload or fails deserialization, that feature is skipped and the rest are applied. This behavior is forgiving but can lead to inconsistent state.

Enable strict mode to require all registered feature states to be present and valid in the incoming payload:

reservoir.AddReservoirDevTools(options =>
{
options.Enablement = ReservoirDevToolsEnablement.DevelopmentOnly;
options.IsStrictStateRehydrationEnabled = true;
});

Strict Mode Behavior

When IsStrictStateRehydrationEnabled is true:

  • Time-travel operations (jump, reset, import) succeed only if ALL registered feature states can be deserialized from the payload
  • If any feature state is missing or fails deserialization, the entire operation is rejected
  • The current state remains unchanged when rejection occurs

When IsStrictStateRehydrationEnabled is false (default):

  • Missing features are skipped
  • Features that fail deserialization are skipped
  • Successfully deserialized features are applied

(ReduxDevToolsService.TryRestoreStateFromJsonDocument)

When to Use Strict Mode

Use strict mode when:

  • Your application requires all feature states to be in sync
  • You want to prevent partial state application that could cause inconsistencies
  • You are debugging issues where partial rehydration masks problems

Use best-effort mode (default) when:

  • You want maximum flexibility during development
  • Your feature states are independently valid
  • You are iterating quickly and feature state schemas may change

Sanitizers

Use sanitizers to transform actions or state before sending to DevTools. This is useful for removing sensitive data (passwords, tokens, PII) or reducing payload size for large states.

Delegate Signatures

// ActionSanitizer: Transform an action before sending to DevTools
Func<IAction, object?>? ActionSanitizer

// StateSanitizer: Transform the state snapshot before sending to DevTools
Func<IReadOnlyDictionary<string, object>, object?>? StateSanitizer

Return Value Semantics

Return ValueBehavior
nullUse default serialization (action or state is sent as-is)
Any objectThe returned object is serialized and sent instead

Action Sanitizer

Receives each dispatched IAction and can return a replacement object for DevTools display:

reservoir.AddReservoirDevTools(options =>
{
options.ActionSanitizer = action =>
{
// Redact sensitive fields from specific action types
if (action is LoginAction login)
{
return new { type = "LoginAction", username = login.Username, password = "***" };
}

// Return null to use default serialization for other actions
return null;
};
});

State Sanitizer

Receives the full state snapshot (keyed by feature key) and can return a modified snapshot:

reservoir.AddReservoirDevTools(options =>
{
options.StateSanitizer = state =>
{
// Remove entire feature states containing sensitive data
var sanitized = new Dictionary<string, object>(state);
sanitized.Remove("auth");
sanitized.Remove("userProfile");
return sanitized;
};
});
tip

Sanitizers run on every action dispatch. Keep them fast to avoid impacting application performance. Consider caching sanitizer logic or using pattern matching for efficiency.

Summary

ConceptDescription
ArchitectureScoped service subscribing to IStore.StoreEvents, initialized via component
RegistrationAddReservoirDevTools() after AddReservoir()
InitializationAdd <ReservoirDevToolsInitializerComponent/> to App.razor
Missing Component DetectionHosted service checks initialization after 5 seconds; behavior controlled by ThrowOnMissingInitializer
EnablementOff (default), DevelopmentOnly, or Always
Connection LoggingLogs Information on successful connection; Warning on failure
Time-travelCommands become system actions (RestoreStateAction, ResetToInitialStateAction)
Strict modeIsStrictStateRehydrationEnabled requires all features in time-travel payloads
SanitizersTransform actions/state before sending to DevTools

Next Steps

  • Store - Understand the observable store events and system actions that DevTools uses
  • Reservoir Overview - Learn the dispatch pipeline that DevTools observes
  • Testing - Test reducers and effects without DevTools