Blazor's component model is built on six interlocking concepts. Each one is simple in isolation. The power — and the complexity that trips up developers — comes from how they interact. This guide presents all six together, then synthesises them into a single working component you can use as a reference.
Part 1 — Binding
Binding is how C# state becomes visible in the DOM, and how user input flows back into C#. There are three modes:
- One-way (
@variable) — renders a C# expression. Re-renders whenever the component re-renders. The DOM never writes back. - Two-way (
@bind) — desugars to a value attribute plus an@onchangehandler. Updates fire on blur, not on every keystroke. - Real-time (
@bind:event="oninput") — updates on every keystroke. Required for live search, character counters, and similar real-time scenarios.
<input @bind="searchTerm" @bind:event="oninput" placeholder="Search…" />
<p>Results: @FilteredItems.Count()</p>
@code {
private string searchTerm = "";
private IEnumerable<Student> FilteredItems =>
students.Where(s => s.Name.Contains(searchTerm, StringComparison.OrdinalIgnoreCase));
}
Part 2 — Lifecycle
Every component moves through a predictable sequence of events. Putting code in the right event is critical.
| Method | When | Use for |
|---|---|---|
OnInitialized |
Once, before first render | Synchronous default state |
OnInitializedAsync |
Once, async after init | API calls, database queries |
OnParametersSet |
Every parameter change | Reacting to new parameter values |
OnAfterRender(firstRender) |
After every render | JS interop, DOM measurement |
Dispose |
On removal from tree | Stop timers, cancel subscriptions |
The isLoading pattern is standard: set it to true before the await, set it to false in finally, and render a spinner when it is true.
Part 3 — LINQ Computed Properties
Never filter with @if inside @foreach. Move all filtering, sorting, and projection logic into a computed property. LINQ operators use deferred execution — the query runs at enumeration time, not at declaration.
private IEnumerable<Student> FilteredItems =>
students
.Where(s => activeFilter == "All" || s.Status == activeFilter)
.Where(s => s.Name.Contains(searchTerm, StringComparison.OrdinalIgnoreCase))
.OrderBy(s => s.Name);
Daily operators: Any, Count, Where, OrderBy/OrderByDescending, Select().Distinct(), FirstOrDefault. Call .ToList() when you reference the result more than once in a render.
Part 4 — Components and Parameters
A .razor file compiles to a C# class extending ComponentBase. Extract a component when markup repeats or grows unwieldy.
[Parameter]— exposes a property as a parent-settable attribute.[Parameter, EditorRequired]— compile-time warning when the attribute is omitted.- Never mutate a
[Parameter]property inside the child — the parent owns it. - Computed properties keep presentation logic out of the template.
// StudentCard.razor
[Parameter, EditorRequired] public Student Student { get; set; } = default!;
[Parameter] public bool IsSelected { get; set; }
private string CardClass => IsSelected ? "card card--selected" : "card";
Part 5 — EventCallback vs Action
Child-to-parent communication uses EventCallback<T>. It automatically triggers a parent re-render, is awaitable, and is null-safe. Action<T> does none of these things — never use it for component callbacks.
| Feature | EventCallback<T> | Action<T> |
|---|---|---|
| Auto re-render parent | Yes | No |
| Awaitable | Yes | No |
| Null-safe InvokeAsync | Yes | No |
Part 6 — Cascading Values
When data needs to reach a deeply nested component, prop-drilling (passing it through every intermediate layer) couples all those layers to the data type. Cascading values broadcast a value to an entire subtree; any descendant receives it with [CascadingParameter] — no changes needed in the middle layers.
<CascadingValue Value="currentTheme">
<StudentList />
</CascadingValue>
Use IsFixed="true" for static cascades (app config, culture) to prevent unnecessary re-render checks across the subtree.
Synthesizing It All: StudentList Component
The following component uses all six concepts in a single, realistic piece of code. Read the comments to see where each concept applies.
@* StudentList.razor *@
@page "/students"
@implements IDisposable
@* CONCEPT 6 — receive the cascaded theme *@
<CascadingValue Value="currentTheme">
@* CONCEPT 2 — show a spinner during async load *@
@if (isLoading)
{
<p>Loading students…</p>
}
else
{
@* CONCEPT 1 — real-time binding *@
<input @bind="searchTerm" @bind:event="oninput" placeholder="Search by name…" />
<select @bind="activeFilter">
<option value="All">All Statuses</option>
<option value="Passing">Passing</option>
<option value="Failing">Failing</option>
</select>
@* CONCEPT 3 — loop over the computed property, not the raw list *@
@foreach (var student in FilteredStudents)
{
@* CONCEPT 4 — child component with Parameter *@
<StudentCard Student="student"
IsSelected="selectedStudent?.Id == student.Id"
@* CONCEPT 5 — EventCallback wires child click to parent handler *@
/>
}
<p>Showing @FilteredStudents.Count() of @students.Count students</p>
}
</CascadingValue>
@code {
// ── State ───────────────────────────────────────────────
private List<Student> students = new();
private Student? selectedStudent;
private string searchTerm = "";
private string activeFilter = "All";
private bool isLoading = true;
private AppTheme currentTheme = new() { Name = "light", PrimaryColor = "#0078d4" };
// ── CONCEPT 2: Lifecycle — async data load ───────────────
protected override async Task OnInitializedAsync()
{
try
{
isLoading = true;
students = await StudentService.GetAllAsync();
}
finally { isLoading = false; }
}
// ── CONCEPT 3: LINQ computed property ────────────────────
private IEnumerable<Student> FilteredStudents =>
students
.Where(s => activeFilter == "All" || s.Status == activeFilter)
.Where(s => s.Name.Contains(searchTerm, StringComparison.OrdinalIgnoreCase))
.OrderBy(s => s.Name);
// ── CONCEPT 5: EventCallback handler ─────────────────────
private void HandleStudentSelected(Student student) => selectedStudent = student;
// ── CONCEPT 2: Lifecycle — Dispose timer ─────────────────
private System.Timers.Timer? _timer;
protected override void OnInitialized()
{
_timer = new System.Timers.Timer(30_000);
_timer.Elapsed += async (_, _) => await InvokeAsync(StateHasChanged);
_timer.Start();
}
public void Dispose() { _timer?.Stop(); _timer?.Dispose(); }
}
Key Takeaways
- The six pillars — binding, lifecycle, LINQ, components, EventCallback, cascading — are designed to work together, not independently.
- Data flows down via parameters and cascading values; events flow up via EventCallback.
- The parent owns all shared state; children display it and raise events to request changes.
- LINQ computed properties keep templates clean and business logic out of markup.
- Every component with an external subscription must implement
IDisposable. - This reference pattern — real-time bind → async load → LINQ filter → child component → EventCallback → cascaded theme — covers 80% of real Blazor UI patterns.