# UseReducer
*Manage complex [state](./03_UseState.md) logic with reducers, providing a predictable state management pattern for [components](../../../01_Onboarding/02_Concepts/02_Views.md) with multiple sub-values or interdependent state updates.*
## Overview
The `UseReducer` [hook](../01_RulesOfHooks.md) is an alternative to [`UseState`](./03_UseState.md) that is better suited for managing complex [state](./03_UseState.md) logic. It follows the reducer pattern where state updates are handled by a pure function.
Key benefits of `UseReducer`:
- **Predictable [State](./03_UseState.md) Updates** - All state changes go through a single reducer function
- **Complex [State](./03_UseState.md) Logic** - Better suited for state with multiple sub-values or interdependent updates
- **Action-Based Updates** - State changes are explicit and traceable through actions
- **Testability** - Pure reducer functions are easy to test in isolation
## Basic Usage
```csharp
public class BasicReducerDemo : ViewBase
{
// Reducer function
private int CounterReducer(int state, string action) => action switch
{
"increment" => state + 1,
"decrement" => state - 1,
"reset" => 0,
_ => state
};
public override object? Build()
{
var (count, dispatch) = this.UseReducer(CounterReducer, 0);
return Layout.Vertical(
Text.H3($"Count: {count}"),
Layout.Horizontal(
new Button("-", _ => dispatch("decrement")),
new Button("Reset", _ => dispatch("reset")),
new Button("+", _ => dispatch("increment"))
)
);
}
}
```
> **Tip:** `UseReducer` is ideal when you have complex [state](./03_UseState.md) logic involving multiple sub-values, when the next state depends on the previous one, or when you want to centralize state update logic in one place.
## When to Use UseReducer
```mermaid
flowchart TD
A["Need to manage state?"] --> B{What's the complexity?}
B --> C["Simple state<br/>1-3 independent values"]
B --> D["Complex state<br/>4+ values or interdependent"]
B --> E["State updates depend<br/>on previous state"]
B --> F["Need centralized<br/>state logic"]
C --> G["Use UseState<br/>Simple and direct"]
D --> H["Use UseReducer<br/>Better organization"]
E --> I["Use UseReducer<br/>Predictable updates"]
F --> J["Use UseReducer<br/>Single source of truth"]
G --> K["Direct state setting<br/>Less boilerplate<br/>Good for simple cases"]
H --> L["Action-based updates<br/>More structured<br/>Better for complex logic"]
I --> M["Pure reducer function<br/>Easier to reason about<br/>Better testability"]
J --> N["Centralized logic<br/>Easier to maintain<br/>Better debugging"]
```
> **Tip:** Reducers should be pure functions - they should not have side effects and should return a new [state](./03_UseState.md) object rather than mutating the existing one. Use [`UseEffect`](./04_UseEffect.md) for side effects.
### How UseReducer Works
```mermaid
sequenceDiagram
participant C as Component
participant UR as UseReducer Hook
participant R as Reducer Function
participant S as State Storage
Note over C,S: Initialization
C->>UR: UseReducer(reducer, initialState)
UR->>S: Store initialState
UR->>UR: Create dispatch function
UR-->>C: Return (state, dispatch)
Note over C,S: State Update via Dispatch
C->>UR: dispatch(action)
UR->>S: Get current state
S-->>UR: Return current state
UR->>R: Call reducer(currentState, action)
R->>R: Process action and compute new state
R-->>UR: Return new state
UR->>S: Update stored state
UR->>C: Trigger re-render with new state
Note over C,S: Component Re-render
C->>UR: UseReducer(reducer, initialState)
UR->>S: Get current state
S-->>UR: Return updated state
UR-->>C: Return (newState, dispatch)
```
### Use Cases
Use `UseReducer` when:
- **Complex [State](./03_UseState.md) Logic** - Managing state with multiple sub-values or interdependent properties
- **State Updates Depend on Previous State** - When the next state depends on the previous state value
- **Action-Based Updates** - When you want explicit, traceable state changes through actions
- **Centralized State Logic** - When you want to centralize all state update logic in one place
- **Better Testability** - When you need to test state logic in isolation
### UseReducer vs UseState
Choose between `UseReducer` and [`UseState`](./03_UseState.md) based on complexity:
| UseState | UseReducer |
|----------|------------|
| Simple state updates | Complex state logic |
| Independent values | Interdependent values |
| Direct state setting | Action-based updates |
| Less boilerplate | More structured |
| Good for 1-3 values | Good for 4+ values |
### Best Practices
- **Keep Reducers Pure** - Reducers should not have side effects and should return new [state](./03_UseState.md) objects. Use [`UseEffect`](./04_UseEffect.md) for side effects.
- **Use Immutable Updates** - Always return new state objects rather than mutating existing ones
- **Handle All Action Types** - Include a default case to handle unknown actions gracefully
- **Type Safety** - Use strongly-typed actions and [state](./03_UseState.md) for better compile-time safety
- **Extract Complex Logic** - Move complex reducer logic into separate functions for clarity and use [`UseMemo`](./05_UseMemo.md) for expensive computations
## See Also
- [State Management](./03_UseState.md) - Simple state management with UseState
- [Rules of Hooks](../02_RulesOfHooks.md) - Understanding hook rules and best practices
- [Effects](./04_UseEffect.md) - Side effects and async operations
- [Memoization](./05_UseMemo.md) - Performance optimization with UseMemo
- [Callbacks](./06_UseCallback.md) - Memoized callback functions with UseCallback
- [Views](../../../01_Onboarding/02_Concepts/02_Views.md) - Understanding Ivy views and components
### Examples
### Shopping Cart with Interdependent State
This example demonstrates why reducers are powerful - multiple interdependent values (items, subtotal, tax, discount, total) that must stay in sync. With `UseState`, you'd need to update each value separately and risk inconsistencies.
```csharp
public class ShoppingCartDemo : ViewBase
{
record CartItem(string Name, decimal Price, int Quantity);
record CartState(List<CartItem> Items, decimal DiscountPercent, decimal Subtotal, decimal Tax, decimal Total);
private CartState CartReducer(CartState state, string action)
{
var parts = action.Split('|', 2);
var actionType = parts[0];
var actionValue = parts.Length > 1 ? parts[1] : "";
return actionType switch
{
"addItem" => CalculateTotals(state with {
Items = state.Items.Append(new CartItem(actionValue, 10.00m, 1)).ToList()
}),
"removeItem" => CalculateTotals(state with {
Items = state.Items.Where((item, idx) => idx.ToString() != actionValue).ToList()
}),
"setDiscount" => CalculateTotals(state with {
DiscountPercent = decimal.TryParse(actionValue, out var discount) ? discount : 0m
}),
_ => state
};
}
private CartState CalculateTotals(CartState state)
{
var subtotal = state.Items.Sum(item => item.Price * item.Quantity);
var discountAmount = subtotal * (state.DiscountPercent / 100m);
var afterDiscount = subtotal - discountAmount;
var tax = afterDiscount * 0.08m; // 8% tax
var total = afterDiscount + tax;
return state with
{
Subtotal = subtotal,
Tax = tax,
Total = total
};
}
public override object? Build()
{
var (cart, dispatch) = this.UseReducer(CartReducer, new CartState(new List<CartItem>(), 0m, 0m, 0m, 0m));
var newItemName = UseState("");
return Layout.Vertical(
Text.H3("Shopping Cart"),
Layout.Horizontal(
newItemName.ToTextInput().Placeholder("Item name"),
new Button("Add Item", _ => {
if (!string.IsNullOrWhiteSpace(newItemName.Value))
{
dispatch($"addItem|{newItemName.Value}");
newItemName.Set("");
}
})
),
new Separator(),
cart.Items.Count > 0
? Layout.Vertical(
new {
Items = string.Join(", ", cart.Items.Select(item => $"{item.Name} (${item.Price:F2} x {item.Quantity})"))
}.ToDetails(),
Layout.Horizontal(
cart.Items.Select((item, idx) =>
new Button($"Remove {item.Name}", _ => dispatch($"removeItem|{idx}"))
).ToArray()
)
)
: Text.P("No items in cart").Small(),
new Separator(),
Layout.Vertical(
Text.P($"Subtotal: ${cart.Subtotal:F2}"),
Text.P($"Discount ({cart.DiscountPercent}%): ${cart.Subtotal * (cart.DiscountPercent / 100m):F2}"),
Text.P($"Tax (8%): ${cart.Tax:F2}"),
Text.H4($"Total: ${cart.Total:F2}")
),
Layout.Horizontal(
new Button("10% Off", _ => dispatch("setDiscount|10")),
new Button("20% Off", _ => dispatch("setDiscount|20")),
new Button("Clear Discount", _ => dispatch("setDiscount|0"))
)
);
}
}
```
### Game State with Multiple Interdependent Values
This example shows a game state where actions affect multiple values simultaneously - perfect for demonstrating reducer's centralized state management.
```csharp
public class GameStateDemo : ViewBase
{
record GameState(int Score, int Level, int Lives, int Multiplier, bool IsGameOver);
private GameState GameReducer(GameState state, string action) => action switch
{
"score" => state.IsGameOver ? state : state with {
Score = state.Score + (10 * state.Multiplier),
Level = (state.Score + (10 * state.Multiplier)) / 100 + 1,
Multiplier = Math.Min(state.Multiplier + 1, 5)
},
"miss" => state.IsGameOver ? state : state with {
Lives = state.Lives - 1,
Multiplier = 1,
IsGameOver = state.Lives <= 1
},
"reset" => new GameState(0, 1, 3, 1, false),
_ => state
};
public override object? Build()
{
var (game, dispatch) = this.UseReducer(GameReducer, new GameState(0, 1, 3, 1, false));
return Layout.Vertical(
Text.H3("Game State"),
Layout.Vertical(
Text.P($"Score: {game.Score}"),
Text.P($"Level: {game.Level}"),
Text.P($"Lives: {game.Lives}"),
Text.P($"Multiplier: {game.Multiplier}x"),
game.IsGameOver ? Text.Danger("Game Over!") : Text.Success("Playing...")
),
new Separator(),
Layout.Horizontal(
new Button("Score!", _ => dispatch("score")).Disabled(game.IsGameOver),
new Button("Miss", _ => dispatch("miss")).Disabled(game.IsGameOver),
new Button("Reset", _ => dispatch("reset"))
),
Text.P("Notice how scoring updates level and multiplier, while missing resets multiplier and decreases lives - all in one reducer!").Small()
);
}
}
```