Skip to main content

Action Reducers

Overview

Action reducers are pure functions that take the current state and an action, and return a new state. They are the synchronous heart of Reservoir's state management.

What Is a Reducer?

A reducer answers the question: "Given this state and this action, what is the new state?"

public static EntitySelectionState SetEntityId(EntitySelectionState state, SetEntityIdAction action)
=> state with
{
EntityId = string.IsNullOrEmpty(action.EntityId) ? null : action.EntityId,
};

Reducers must be pure functions:

  • Given the same state and action, they always return the same new state
  • They must not have side effects (no HTTP calls, no logging, no mutations)

(IActionReducer)

Registration Options

Reservoir provides two ways to register reducers, depending on your preference.

Option 1: Delegate Reducers

Register a static method or lambda directly with AddReducer:

// Using a static method from a reducer class
services.AddReducer<SetEntityIdAction, EntitySelectionState>(EntitySelectionReducers.SetEntityId);

This approach uses DelegateActionReducer<TAction, TState> internally.

This option registers a delegate-based reducer for the action/state pair. (AddReducer overloads)

Option 2: Class-Based Reducers

Create a class that inherits from ActionReducerBase<TAction, TState> and register it with the three-type-parameter overload:

// Example skeleton (replace MyAction/MyState with your types)
public sealed class MyReducer : ActionReducerBase<MyAction, MyState>
{
public override MyState Reduce(MyState state, MyAction action)
=> state;
}

// Registration
services.AddReducer<MyAction, MyState, MyReducer>();

This option registers the reducer class as a transient service and composes it into the root reducer. (AddReducer overloads)

Organizing Reducers

A common pattern is to group reducer functions in a static class per feature:

// EntitySelectionReducers.cs
internal static class EntitySelectionReducers
{
public static EntitySelectionState SetEntityId(
EntitySelectionState state,
SetEntityIdAction action
) =>
state with
{
EntityId = string.IsNullOrEmpty(action.EntityId) ? null : action.EntityId,
};

// Add more reducer methods as needed for this feature.
}

Then register each reducer separately:

services.AddReducer<SetEntityIdAction, EntitySelectionState>(EntitySelectionReducers.SetEntityId);

(Spring sample: EntitySelectionReducers)

How Reducers Are Invoked

When an action is dispatched, the store calls RootReducer<TState> for each registered feature state. The root reducer:

  1. Looks up reducers registered for the action's exact type
  2. Calls each matching reducer in registration order
  3. Passes the output of one reducer as input to the next

Multiple reducers can handle the same action type. They run in sequence, each receiving the state produced by the previous reducer.

(RootReducer.Reduce)

State Updates

Reducers return a new state instance when state changes. Feature states are expected to be immutable records, so the C# with expression is a common update pattern. (IFeatureState, Spring sample reducer)

// ✅ Correct: use `with` to create new state
public static EntitySelectionState SetEntityId(EntitySelectionState state, SetEntityIdAction action)
=> state with
{
EntityId = string.IsNullOrEmpty(action.EntityId) ? null : action.EntityId,
};

// Use `with` to update only the properties that changed

If the reducer doesn't need to change state, return the original state instance unchanged:

// Example skeleton (replace MyState/SomeAction with your types)
public static MyState MaybeUpdate(MyState state, SomeAction action)
{
if (!action.ShouldApply)
return state; // No change-return same instance

return state with { Value = action.NewValue };
}

Automatic Registration Side Effects

Both AddReducer overloads automatically:

  1. Register the IRootReducer<TState> that composes all reducers for the feature
  2. Register the IFeatureStateRegistration that provides initial state

You don't need to call AddFeatureState or AddRootReducer separately when using AddReducer.

(ReservoirRegistrations)

Summary

ConceptDescription
ReducerPure function: (state, action) => newState
Delegate registrationAddReducer<TAction, TState>(func)
Class registrationAddReducer<TAction, TState, TReducer>()
Base classActionReducerBase<TAction, TState> handles type checking
ImmutabilityFeature states are expected to be immutable records; reducers return new instances when state changes

Next Steps

  • Reservoir Overview - Understand how reducers fit into the dispatch pipeline
  • Effects - Handle async operations triggered by actions
  • Feature State - Organize state into feature slices
  • Store - Understand the central hub that coordinates reducers, effects, and state