Files
Nexus.Reader/.agent/skills/blazor-expert/resources/components-lifecycle.md
T
Antigravity 5a2223a4c8 feat: Ingestion Pipeline Stabilization and WASM Service Proxies (#42)
This PR stabilizes the Nexus Ingestion Engine by implementing functional service proxies for the Blazor WASM client and refining the backend infrastructure for real-time progress tracking and database compatibility.

### Key Changes
- **Infrastructure Stabilization**:
  - Implemented production-grade `EbookRepository` with PostgreSQL `EF.Functions.ILike` support.
  - Enforced `IsReadyForReading = false` state for newly added ebooks (resolves #35).
  - Updated `SignalRSyncBroadcaster` to support targeted user messaging and ingestion-specific progress updates (resolves #37).
- **WASM Client Functional Proxies**:
  - Replaced "Throwing" dummy services with `WasmEbookRepository`, `WasmSyncBroadcaster`, `WasmBookStorageService`, and `WasmEmbeddingGenerator`.
  - These services proxy requests to the backend via a new set of Minimal API endpoints in `NexusReader.Web`.
- **Domain Refinement**:
  - Added `IsReadyForReading` flag to the `Ebook` entity to manage background AI processing states.

### Related Issues
- Fixes #35
- Fixes #36
- Fixes #37

---------

Co-authored-by: Marek Jasiński <jasins.marek@gmail.com>
Reviewed-on: #42
Co-authored-by: Antigravity <antigravity@google.com>
Co-committed-by: Antigravity <antigravity@google.com>
2026-05-13 18:24:24 +00:00

551 lines
12 KiB
Markdown

# Blazor Components & Component Lifecycle
## Component Structure
Components are the fundamental building blocks of Blazor applications. A Blazor component is a self-contained piece of UI with an optional logic.
### Basic Component Syntax
```csharp
@page "/example"
@using MyApp.Services
@inject IMyService MyService
<h3>@Title</h3>
<div>@ChildContent</div>
<button @onclick="HandleClick">Click me</button>
@code {
[Parameter]
public string Title { get; set; } = "Default";
[Parameter]
public RenderFragment? ChildContent { get; set; }
private void HandleClick()
{
// Handle button click
}
}
```
Key elements:
- **`@page` directive**: Makes component routable (optional for non-page components)
- **`@using`**: Import namespaces
- **`@inject`**: Dependency injection
- **HTML markup**: Regular HTML with Blazor directives
- **`@code` block**: C# logic including lifecycle methods
### Component vs Page
- **Page Component**: Has `@page` directive, routable via URL
- Example: `/Counter` route
- Located in `Pages/` folder (convention)
- **Reusable Component**: No `@page` directive, used by other components
- Example: `<UserCard @bind-User="user" />`
- Located in `Shared/` or domain-specific folder
## Component Lifecycle
### Lifecycle Sequence
Component lifecycle methods execute in this order:
```
1. SetParametersAsync()
2. OnInitialized() or OnInitializedAsync()
3. OnParametersSet() or OnParametersSetAsync()
4. ShouldRender() [decision point - skip if returns false]
5. OnAfterRender() or OnAfterRenderAsync()
```
When parameters change (parent re-renders):
```
SetParametersAsync() [parameters updated]
OnParametersSet() [NOT OnInitialized - that runs once only]
ShouldRender()
OnAfterRender()
```
### Lifecycle Methods Detailed
#### SetParametersAsync()
- **When**: First method called, before initialization
- **Purpose**: Set component parameters
- **Usage**: Rarely overridden, use OnInitialized instead
- **Code Example**:
```csharp
public override async Task SetParametersAsync(ParameterView parameters)
{
// Custom parameter processing if needed
await base.SetParametersAsync(parameters);
}
```
#### OnInitialized / OnInitializedAsync()
- **When**: Once per component lifetime, after parameters set
- **Purpose**: Initialize component state, load data
- **Runs**: Only ONCE, even if parameters change
- **Code Example**:
```csharp
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
data = await Service.LoadDataAsync();
}
```
**Common Uses:**
- Load initial data from API
- Set up subscriptions
- Initialize state based on parameters
#### OnParametersSet / OnParametersSetAsync()
- **When**: After parameters set, runs EVERY time parameters change
- **Purpose**: React to parameter changes
- **Runs**: Every time parent re-renders with different values
- **Code Example**:
```csharp
protected override async Task OnParametersSetAsync()
{
await base.OnParametersSetAsync();
if (UserId != previousUserId)
{
data = await Service.LoadUserDataAsync(UserId);
previousUserId = UserId;
}
}
```
**Common Uses:**
- Update UI based on new parameter values
- Fetch new data when ID parameter changes
- React to cascading parameter changes
#### ShouldRender()
- **When**: Before DOM rendering, decision point
- **Purpose**: Optimize rendering by skipping unnecessary renders
- **Returns**: true (render) or false (skip)
- **Code Example**:
```csharp
protected override bool ShouldRender()
{
// Only render if specific field changed
return hasChanged;
}
```
**Common Optimizations:**
- Skip render if data unchanged
- Prevent re-render from external events
- Implement custom change detection
#### OnAfterRender / OnAfterRenderAsync()
- **When**: After component rendered to DOM
- **Purpose**: Work with DOM, initialize JS libraries, final setup
- **Parameter**: `firstRender` - true only on first render
- **Code Example**:
```csharp
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Initialize JS library only once
await JS.InvokeVoidAsync("initializeChart", elementRef);
}
}
```
**Critical Use Case:**
```csharp
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
// Load JS module
module = await JS.InvokeAsync<IJSObjectReference>(
"import", "./scripts/myScript.js");
// Initialize library
await module.InvokeVoidAsync("setupChart", element);
}
}
```
**Important:** Always use `firstRender` check for one-time initialization. This prevents re-initializing on every parameter change.
## Component Parameters
### Parameter Declaration
```csharp
@code {
// Simple parameter
[Parameter]
public string Title { get; set; } = "Default";
// Required parameter (C# 11+)
[Parameter, EditorRequired]
public int UserId { get; set; }
// Child content
[Parameter]
public RenderFragment? ChildContent { get; set; }
// Event callback
[Parameter]
public EventCallback<string> OnValueChanged { get; set; }
// Cascading parameter
[CascadingParameter]
public ThemeInfo? CurrentTheme { get; set; }
}
```
### Parameter Best Practices
**Use Clear Names:**
```csharp
// ✅ Good - clear intent
[Parameter]
public bool IsVisible { get; set; }
// ❌ Poor - ambiguous
[Parameter]
public bool State { get; set; }
```
**Use Nullable Types for Optional:**
```csharp
// ✅ Good - nullable indicates optional
[Parameter]
public string? OptionalValue { get; set; }
// ✅ Good - default value
[Parameter]
public int MaxItems { get; set; } = 10;
// ❌ Poor - not clear if optional
[Parameter]
public string RequiredValue { get; set; }
```
**Use [EditorRequired] for Required Parameters (C# 11+):**
```csharp
// ✅ Best practice - compiler enforces, IDE warns
[Parameter, EditorRequired]
public string Title { get; set; } = default!;
// Fallback for older C#
[Parameter]
public string Title { get; set; } = default!;
```
**Use EventCallback for Child-to-Parent Communication:**
```csharp
// ✅ Correct - EventCallback for async safety
[Parameter]
public EventCallback<string> OnValueChanged { get; set; }
// ✅ With custom args
[Parameter]
public EventCallback<ValueChangeEventArgs> OnValueChanged { get; set; }
// ❌ Avoid - direct Action, not async-safe
[Parameter]
public Action<string>? OnValueChanged { get; set; }
```
### Parameter Change Detection
To know when a parameter changed:
```csharp
@code {
private int previousUserId;
[Parameter]
public int UserId { get; set; }
protected override async Task OnParametersSetAsync()
{
if (UserId != previousUserId)
{
previousUserId = UserId;
await LoadUserData();
}
}
}
```
Or use a comparison strategy:
```csharp
private object? previousCriteria;
protected override async Task OnParametersSetAsync()
{
var currentCriteria = (SearchId, SearchTerm);
if (!Equals(previousCriteria, currentCriteria))
{
previousCriteria = currentCriteria;
await PerformSearch();
}
}
```
## Cascading Values
Cascading values allow ancestor components to provide data to all descendants without explicit parameter passing.
### Providing Cascading Values
```csharp
<!-- Parent component -->
<CascadingValue Value="@currentUser">
@ChildContent
</CascadingValue>
@code {
[Parameter]
public RenderFragment? ChildContent { get; set; }
private User currentUser = new();
}
```
### Receiving Cascading Values
```csharp
<!-- Child component anywhere in hierarchy -->
@code {
[CascadingParameter]
public User? CurrentUser { get; set; }
protected override void OnInitialized()
{
if (CurrentUser == null)
{
// Handle missing cascading value
}
}
}
```
### Multiple Cascading Values
```csharp
<!-- Provider -->
<CascadingValue Value="@theme">
<CascadingValue Value="@currentUser">
<CascadingValue Value="@permissions">
@ChildContent
</CascadingValue>
</CascadingValue>
</CascadingValue>
<!-- Consumer - multiple parameters -->
@code {
[CascadingParameter]
public Theme? Theme { get; set; }
[CascadingParameter]
public User? CurrentUser { get; set; }
[CascadingParameter]
public Permissions? Permissions { get; set; }
}
```
### Named Cascading Values
For disambiguation when multiple values of same type:
```csharp
<!-- Provider -->
<CascadingValue Value="@themeLight" Name="Light">
<CascadingValue Value="@themeDark" Name="Dark">
@ChildContent
</CascadingValue>
</CascadingValue>
<!-- Consumer -->
@code {
[CascadingParameter(Name = "Light")]
public Theme? LightTheme { get; set; }
[CascadingParameter(Name = "Dark")]
public Theme? DarkTheme { get; set; }
}
```
## RenderFragment for Component Composition
RenderFragment enables flexible component composition.
### Basic RenderFragment
```csharp
<!-- Parent component -->
<div>
<h2>Header</h2>
@ChildContent
<footer>Footer</footer>
</div>
@code {
[Parameter]
public RenderFragment? ChildContent { get; set; }
}
<!-- Usage -->
<Layout>
<p>This is the main content</p>
</Layout>
```
### Typed RenderFragment with Context
```csharp
<!-- ListComponent.razor -->
@foreach (var item in Items)
{
@ItemTemplate(item)
}
@code {
[Parameter]
public IEnumerable<Item> Items { get; set; } = [];
[Parameter]
public RenderFragment<Item>? ItemTemplate { get; set; }
}
<!-- Usage -->
<ListComponent Items="@items">
<ItemTemplate Context="item">
<div>@item.Name - @item.Price</div>
</ItemTemplate>
</ListComponent>
```
### Multiple Named Content Areas
```csharp
<!-- Card component with multiple slots -->
<div class="card">
<div class="card-header">@Header</div>
<div class="card-body">@Body</div>
<div class="card-footer">@Footer</div>
</div>
@code {
[Parameter]
public RenderFragment? Header { get; set; }
[Parameter]
public RenderFragment? Body { get; set; }
[Parameter]
public RenderFragment? Footer { get; set; }
}
<!-- Usage -->
<Card>
<Header>
<h3>Card Title</h3>
</Header>
<Body>
<p>Card content</p>
</Body>
<Footer>
<button>Action</button>
</Footer>
</Card>
```
## Component Best Practices
### Single Responsibility
- Each component should have one clear purpose
- Avoid god components that do too much
- Example: `UserProfile` component should focus on displaying user info, not handle complex business logic
### Composition Over Inheritance
- Use cascading values for shared state, not deep hierarchies
- Compose components rather than creating base classes
- Example: Create theme provider component instead of theme-aware base class
### Keep Components Simple
- Minimize `@code` block logic
- Move complex logic to services
- Example: Validation logic → ValidationService, not in component
### Proper Disposal
- Implement `IDisposable` or `IAsyncDisposable`
- Unsubscribe from events
- Dispose timers and resources
```csharp
@implements IAsyncDisposable
@inject IJSRuntime JS
private IJSObjectReference? module;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
module = await JS.InvokeAsync<IJSObjectReference>(
"import", "./myScript.js");
}
}
async ValueTask IAsyncDisposable.DisposeAsync()
{
if (module is not null)
{
await module.DisposeAsync();
}
}
```
---
**Related Resources:** See [state-management-events.md](state-management-events.md) for event handling and state updates. See [performance-advanced.md](performance-advanced.md) for optimization techniques.