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 @onchange handler. 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 parentYesNo
AwaitableYesNo
Null-safe InvokeAsyncYesNo

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.