# Blazor Lifecycle: Pitfalls & Solutions

> **Scope:** blazor-azure
> **Layer:** 2 (on keyword)
> **Keywords:** blazor, lifecycle, oninit, onafter, onparametersset, dispose
> **Load When:** blazor work

## Lifecycle Flow

```
1. SetParametersAsync
2. OnInitialized(Async)       ← No JS available (prerendering)
3. OnParametersSet(Async)
4. BuildRenderTree (Render)
5. OnAfterRender(Async)       ← JS available (firstRender=true)
6. [User Interaction] → 3→4→5 (firstRender=false)
7. Dispose (component removed)
```

## Decision Matrix

| Need to... | Use Method | Note |
|------------|------------|------|
| Load data from API/DB | `OnInitializedAsync` | No JS available |
| Call JavaScript / DOM | `OnAfterRenderAsync(firstRender)` | firstRender guard |
| React to parameter changes | `OnParametersSetAsync` | Called on every change |
| Clean up resources | `Dispose` | Timers, subscriptions |
| Update UI after async op | `StateHasChanged()` | Manual re-render |

## Common Pitfalls

| Problem | Error | Solution |
|---------|-------|----------|
| JSRuntime in OnInitialized | "statically rendered" | Move to `OnAfterRenderAsync(firstRender)` |
| Layout with @rendermode | "Cannot pass Body with rendermode" | Remove `@rendermode` from layouts |
| HttpClient no BaseAddress | "Invalid request URI" | Use `NavigationManager.BaseUri` |
| UI not updating after await | Stale UI | Call `StateHasChanged()` after async |
| File upload > 512KB | "exceeds maximum size" | `OpenReadStream(maxAllowedSize: size)` |
| Dispose not called | Memory leaks | Implement `IDisposable` |
| Cascading param null | NullRef during init | Check null in `OnParametersSet()` |
| Infinite render loop | Browser freezes | Never `StateHasChanged()` in property setters |

---

## Essential Patterns

### JSRuntime (OnAfterRender only)

```csharp
@inject IJSRuntime JSRuntime
@code {
    private bool _initialized;
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && !_initialized)
        {
            await JSRuntime.InvokeVoidAsync("initializeChart", "myChart");
            _initialized = true;
            StateHasChanged();
        }
    }
}
```

### Data Loading

```csharp
@code {
    private List<Item> Items { get; set; } = new();
    private bool Loading = true;
    private string? Error;

    protected override async Task OnInitializedAsync()
    {
        try { Items = await HttpClient.GetFromJsonAsync<List<Item>>("/api/items"); }
        catch (Exception ex) { Error = ex.Message; }
        finally { Loading = false; }
    }
}
```

### Parameter Change

```csharp
@code {
    [Parameter] public int ProductId { get; set; }
    private Product? Product;

    protected override async Task OnParametersSetAsync()
    {
        if (ProductId > 0)
            Product = await HttpClient.GetFromJsonAsync<Product>($"/api/products/{ProductId}");
    }
}
```

### Dispose Pattern

```csharp
@implements IDisposable
@code {
    private System.Timers.Timer? _timer;
    protected override void OnInitialized()
    {
        _timer = new System.Timers.Timer(1000);
        _timer.Elapsed += (s, e) => InvokeAsync(StateHasChanged);
        _timer.Start();
    }
    public void Dispose() { _timer?.Stop(); _timer?.Dispose(); }
}
```

### File Upload

```csharp
@code {
    private const long MaxFileSize = 10 * 1024 * 1024;
    private async Task HandleFile(InputFileChangeEventArgs e)
    {
        if (e.File.Size > MaxFileSize) { Error = "File > 10MB"; return; }
        using var stream = e.File.OpenReadStream(maxAllowedSize: MaxFileSize);
        var buffer = new byte[e.File.Size];
        await stream.ReadAsync(buffer);
    }
}
```

### Infinite Render Loop

```csharp
// WRONG - StateHasChanged in property setter = infinite loop
private int Counter { get => _c; set { _c = value; StateHasChanged(); } }

// CORRECT - StateHasChanged only in event handlers
private int _c;
private void Increment() { _c++; StateHasChanged(); }
```

---

## Performance

| Pattern | Code |
|---------|------|
| Prevent unnecessary renders | `protected override bool ShouldRender() => _dataChanged;` |
| List items with key | `@foreach (var item in Items) { <ItemComponent @key="item.Id" Item="@item" /> }` |
| Debounce input | `_debounce = new Timer(500); _debounce.Elapsed += async (s, a) => await Search(value);` |

---

## Checklist

- [ ] No JSRuntime in `OnInitializedAsync`
- [ ] `StateHasChanged()` after async operations
- [ ] File uploads specify `maxAllowedSize`
- [ ] Dispose pattern for subscriptions/timers
- [ ] No `@rendermode` on layouts
- [ ] Cascading parameters null-checked

---

*MORPH-SPEC by Polymorphism Tech*

---

## Code Review Checklist

Use this checklist during code review for Blazor component lifecycle:

### OnInitializedAsync vs OnAfterRenderAsync

- [ ] **JSRuntime calls are in `OnAfterRenderAsync`, not `OnInitializedAsync`?**
- [ ] **Database/API calls are in `OnInitializedAsync`, not `OnAfterRenderAsync`?**
- [ ] **`firstRender` check is used to prevent duplicate JS calls?**

### StateHasChanged

- [ ] **`StateHasChanged()` called after async operations that update UI?**
- [ ] **No excessive `StateHasChanged()` calls in loops?**

### IDisposable / IAsyncDisposable

- [ ] **Component implements `IDisposable` if it has timers, event handlers, or CTS?**
- [ ] **`CancellationTokenSource` is cancelled and disposed?**
- [ ] **Event handlers are unsubscribed in `Dispose`?**
- [ ] **Timers are stopped and disposed?**

### RenderMode

- [ ] **`@rendermode InteractiveServer` is on pages, NOT on layouts?**
- [ ] **Layout components have NO `@rendermode` directive?**

---

*MORPH-SPEC by Polymorphism Tech*
