# UseMemo
*Memoization helps Ivy [applications](../../../01_Onboarding/02_Concepts/10_Apps.md) run faster by caching results of expensive computations and preventing unnecessary re-renders in your [views](../../../01_Onboarding/02_Concepts/02_Views.md).*
## Overview
Memoization in Ivy provides several powerful tools to optimize performance:
- **[`UseMemo`](#usememo-hook)** - Caches the result of expensive computations
- **[`UseCallback`](./06_UseCallback.md)** - Memoizes callback functions to prevent unnecessary re-renders.
- **`IMemoized`** - Interface for component-level memoization
These [hooks](../01_RulesOfHooks.md) work similarly to their React counterparts (`useMemo`, `useCallback`) but are designed specifically for Ivy's architecture.
## Basic Usage
```csharp
public class ExpensiveCalculationView : ViewBase
{
public override object? Build()
{
var input = UseState(0);
// Memoize the result of an expensive calculation
var result = UseMemo(() =>
{
return input.Value * input.Value;
}, input.Value); // Only recompute when input changes
return Layout.Vertical()
| input.ToNumberInput().Placeholder("Number")
| Text.P($"Result: {result}");
}
}
```
## Choosing the Right Memoization Approach
```mermaid
flowchart TD
A["Need to optimize performance?"] --> B{What are you optimizing?}
B --> C["Expensive computation<br/>or data transformation"]
B --> D["Callback function<br/>passed to child components"]
B --> E["Entire component<br/>with expensive rendering"]
C --> F["Use UseMemo"]
D --> G["Use UseCallback"]
E --> H["Implement IMemoized"]
F --> I["Cache computed values<br/>• Data filtering<br/>• Complex calculations<br/>• Derived state"]
G --> J["Prevent child re-renders<br/>• Event handlers<br/>• Stable function references<br/>• Effect dependencies"]
H --> K["Component-level optimization<br/>• List items<br/>• Heavy UI components<br/>• Custom widgets"]
```
The `UseMemo` [hook](../02_RulesOfHooks.md) caches the result of a computation and only recomputes it when its [state](./03_UseState.md) dependencies change.
> **Tip:** `UseMemo` hook stores only the most recent dependency values for comparison; older values are discarded.
### How UseMemo Works
```mermaid
sequenceDiagram
participant C as Component
participant M as UseMemo Hook
participant S as UseState Storage
Note over C,S: First Render
C->>M: UseMemo(() => expensiveCalc(), [dep1, dep2])
M->>S: UseState(() => new MemoRef(result, deps))
S-->>M: Create new MemoRef with initial value
M->>M: Execute factory function
M->>S: Store MemoRef(result, [dep1, dep2])
M-->>C: Return computed value
Note over C,S: Subsequent Render (deps unchanged)
C->>M: UseMemo(() => expensiveCalc(), [dep1, dep2])
M->>S: Get stored MemoRef
S-->>M: Return MemoRef(cachedResult, [dep1, dep2])
M->>M: AreDependenciesEqual([dep1, dep2], [dep1, dep2])
Note right of M: Dependencies equal!<br/>Skip computation
M-->>C: Return cached value (no computation)
Note over C,S: Subsequent Render (deps changed)
C->>M: UseMemo(() => expensiveCalc(), [dep1_new, dep2])
M->>S: Get stored MemoRef
S-->>M: Return MemoRef(oldResult, [dep1, dep2])
M->>M: AreDependenciesEqual([dep1, dep2], [dep1_new, dep2])
Note right of M: Dependencies changed!<br/>Need recomputation
M->>M: Execute factory function
M->>S: Update MemoRef(newResult, [dep1_new, dep2])
M-->>C: Return new computed value
```
### When to Use Memoization
Use memoization when:
- You have expensive computations that don't need to be redone on every render
- You want to prevent unnecessary re-renders of [child components](../../../01_Onboarding/02_Concepts/03_Widgets.md)
- You're dealing with complex data transformations that depend on [state](./03_UseState.md) changes
- You need stable function references for [`UseEffect`](./04_UseEffect.md) dependencies
## Component Memoization with IMemoized
The `IMemoized` interface allows entire [components](../../../01_Onboarding/02_Concepts/02_Views.md) to be memoized, preventing re-renders when their props haven't changed. This is useful for optimizing [views](../../../01_Onboarding/02_Concepts/02_Views.md) with expensive rendering logic.
### How IMemoized Works
```mermaid
sequenceDiagram
participant P as Parent Component
participant WT as WidgetTree
participant C as IMemoized Component
participant H as Hash Calculator
Note over P,H: First Render
P->>WT: Render child component
WT->>C: Check if implements IMemoized
C-->>WT: Yes, implements IMemoized
WT->>C: Call GetMemoValues()
C-->>WT: Return [prop1, prop2, prop3]
WT->>H: CalculateMemoizedHashCode(viewId, memoValues)
H->>H: Hash viewId + each prop (string/valuetype/json)
H-->>WT: Return computed hash
WT->>C: Call Build() - component renders
WT->>WT: Store TreeNode with memoizedHashCode
Note over P,H: Subsequent Render (props unchanged)
P->>WT: Render child component
WT->>C: Call GetMemoValues()
C-->>WT: Return [prop1, prop2, prop3] (same values)
WT->>H: CalculateMemoizedHashCode(viewId, memoValues)
H-->>WT: Return same hash
WT->>WT: Compare: previousHash == currentHash
Note right of WT: Hash matches!<br/>Skip Build() call
WT-->>P: Reuse previous TreeNode (no re-render)
Note over P,H: Subsequent Render (props changed)
P->>WT: Render child component
WT->>C: Call GetMemoValues()
C-->>WT: Return [prop1_new, prop2, prop3] (changed values)
WT->>H: CalculateMemoizedHashCode(viewId, memoValues)
H-->>WT: Return different hash
WT->>WT: Compare: previousHash != currentHash
Note right of WT: Hash changed!<br/>Need to re-render
WT->>C: Call Build() - component re-renders
WT->>WT: Update TreeNode with new hash
```
### IMemoized Basic Usage
```csharp
public class MemoizedDemoView : ViewBase
{
public override object? Build()
{
var count = UseState(0);
return Layout.Vertical()
| new ExpensiveMemoComponent("Demo", count.Value)
| new Button("Increment", onClick: _ => count.Set(count.Value + 1));
}
}
public class ExpensiveMemoComponent : ViewBase, IMemoized
{
private readonly string _title;
private readonly int _value;
private readonly DateTime _timestamp;
public ExpensiveMemoComponent(string title, int value, DateTime? timestamp = null)
{
_title = title;
_value = value;
_timestamp = timestamp ?? DateTime.Now;
}
public object[] GetMemoValues() => [_title, _value];
public override object? Build() =>
Layout.Vertical()
| Text.H2(_title)
| Text.Block($"Value: {_value}")
| Text.P($"Rendered at: {_timestamp:HH:mm:ss}");
}
```
### IMemoized Component Lifecycle
```mermaid
stateDiagram-v2
[*] --> Created: Component instantiated
Created --> CheckMemo: Parent renders
CheckMemo --> GetValues: Call GetMemoValues()
GetValues --> Compare: Compare with cached values
Compare --> CacheHit: Values unchanged
Compare --> CacheMiss: Values changed
CacheHit --> SkipRender: Use cached result
CacheMiss --> ExecuteBuild: Call Build() method
ExecuteBuild --> UpdateCache: Store new result
UpdateCache --> Rendered: Component updated
SkipRender --> [*]: No re-render needed
Rendered --> [*]: Component rendered
Rendered --> CheckMemo: Next parent render
SkipRender --> CheckMemo: Next parent render
```
### Best Practices for IMemoized
- **Include all relevant props** - Any value that affects rendering should be in `GetMemoValues()`
- **Exclude volatile values** - Don't include timestamps or random values unless they affect the UI
- **Use with .Key()** - Always provide a stable key when rendering memoized components in lists
- **Keep it simple** - Only memoize components with expensive rendering logic
## Common Pitfalls and Solutions
### Unstable Dependencies
**Problem**: Creating new objects or arrays in the dependency array
```csharp
// Bad: New array created on every render
var result = UseMemo(() => ProcessData(data.Value), data.Value, new[] { "option1", "option2" });
```
**Solution**: Use stable references with [UseRef](./08_UseRef.md)
```csharp
// Good: Stable dependency
var options = UseRef(new[] { "option1", "option2" });
var result = UseMemo(() => ProcessData(data.Value), data.Value, options);
```
### Callback Dependencies Issues
**Problem**: [UseCallback](./06_UseCallback.md) callbacks that capture too many state variables
```csharp
// Bad: Callback recreated whenever any state changes
var handleClick = UseCallback(() =>
{
DoSomething(data.Value, filter.Value, sortOrder.Value);
}, data, filter, sortOrder); // Too many dependencies
```
**Solution**: Split into smaller, focused callbacks using [UseCallback](./06_UseCallback.md)
```csharp
// Good: Separate callbacks with minimal dependencies
var handleDataAction = UseCallback(() => DoSomethingWithData(data.Value), data);
var handleFilterAction = UseCallback(() => ApplyFilter(filter.Value), filter);
```
## Best Practices
- **Dependency Array**: Always specify the [state](./03_UseState.md) dependencies that should trigger a recomputation
- **Expensive Operations**: Only memoize truly expensive operations
- **Clean Dependencies**: Keep the dependency array minimal and focused on state values
- **Avoid Side Effects**: Memoized functions should be pure and not have side effects (use [UseEffect](./04_UseEffect.md) for side effects)
## See Also
- [State Management](./03_UseState.md) - Managing component state
- [UseCallback](./06_UseCallback.md) - Memoizing callback functions
- [Effects](./04_UseEffect.md) - Performing side effects with dependencies
- [Rules of Hooks](../02_RulesOfHooks.md) - Understanding hook rules and best practices
- [UseRef](./08_UseRef.md) - Storing stable references
- [Signals](./10_UseSignal.md) - Reactive state management
- [Views](../../../01_Onboarding/02_Concepts/02_Views.md) - Understanding Ivy views and components
## Examples
### Complex Data Filtering
```csharp
public record FilterItem(int Id, string Name);
public class DataFilterDemoView : ViewBase
{
public override object? Build()
{
var data = UseState(new List<FilterItem>
{
new(1, "Laptop"),
new(2, "Mouse"),
new(3, "Keyboard"),
new(4, "Monitor"),
new(5, "Headphones")
});
var filter = UseState("");
var filteredData = UseMemo(() =>
data.Value
.Where(item => item.Name.Contains(filter.Value, StringComparison.OrdinalIgnoreCase))
.ToList(),
data.Value, filter.Value);
var items = filteredData.Count == 0
? new object[] { Text.P("No matches.").Muted() }
: filteredData.Select(i => Text.Block(i.Name)).ToArray();
return Layout.Vertical()
| filter.ToTextInput().Placeholder("Filter by name")
| Layout.Vertical(items);
}
}
```
### Computed Properties
```csharp
public record DemoSale(decimal Amount);
public class StatsDemoView : ViewBase
{
public override object? Build()
{
var sales = UseState(new List<DemoSale> { new(100m), new(250m), new(75m) });
var stats = UseMemo(() => new
{
Total = sales.Value.Sum(s => s.Amount),
Average = sales.Value.Count > 0 ? sales.Value.Average(s => s.Amount) : 0m,
Count = sales.Value.Count
}, sales.Value);
return Layout.Vertical()
| Text.P($"Total: ${stats.Total:N2}")
| Text.P($"Average: ${stats.Average:N2}")
| Text.P($"Count: {stats.Count}")
| new Button("Add sale", onClick: _ => sales.Set(sales.Value.Append(new DemoSale((Random.Shared.Next(1, 50) * 10))).ToList()));
}
}
```
### IMemoized In List Items
```csharp
public record ListProduct(int Id, string Name, decimal Price);
public class ProductListDemoView : ViewBase
{
public override object? Build()
{
var products = UseState(new List<ListProduct>
{
new(1, "Laptop", 999m),
new(2, "Mouse", 29.99m),
new(3, "Keyboard", 79m),
new(4, "Monitor", 299m),
new(5, "Headphones", 149m)
});
var sortBy = UseState("name");
var sortOptions = new IAnyOption[]
{
new Option<string>("Name", "name"),
new Option<string>("Price", "price"),
new Option<string>("Id", "id")
};
var sortedProducts = UseMemo(() =>
(sortBy.Value switch
{
"name" => products.Value.OrderBy(p => p.Name),
"price" => products.Value.OrderBy(p => p.Price),
_ => products.Value.OrderBy(p => p.Id)
}).ToList(),
products.Value, sortBy.Value);
var items = sortedProducts.Select((p, i) => new ProductListCard(p, i).Key(p.Id)).ToArray();
return Layout.Vertical()
| sortBy.ToSelectInput(sortOptions).Placeholder("Sort by")
| Layout.Vertical(items);
}
}
public class ProductListCard : ViewBase, IMemoized
{
private readonly ListProduct _product;
private readonly int _index;
public ProductListCard(ListProduct product, int index)
{
_product = product;
_index = index;
}
public object[] GetMemoValues() => [_product.Id, _product.Name, _product.Price, _index];
public override object? Build() =>
new Card(
Layout.Horizontal()
| new Avatar(_product.Name.Length > 0 ? _product.Name[0].ToString() : "?", null)
| (Layout.Vertical()
| Text.H2(_product.Name)
| Text.Block($"${_product.Price:N2}")
| Text.P($"Position: {_index + 1}").Small()));
}
```