Skip to main content
ANVISoftware Solutions
Lesson 4 of 14Intermediate18 min

Building Components

By the end of this lesson

Create components that render data and respond to interaction.

A component file has two parts. Markup at the top: HTML with C# expressions mixed into it. A @code block at the bottom: an ordinary C# class body with fields, properties and methods.

When the component renders, Blazor evaluates the expressions and produces HTML. When an event fires, it calls your method and then renders the component again. Those two sentences cover most of what happens, and the example below is an application of them.

Components/Pages/Employees.razor
C#
@page "/employees"
@rendermode InteractiveServer

<h1>Employees</h1>

<label for="search">Search by name</label>
<input id="search" value="@searchTerm" @oninput="SearchChanged" />

@if (Visible.Count == 0)
{
    <p role="status">No employees match that search.</p>
}
else
{
    <table>
        <caption>Showing @Visible.Count of @employees.Count employees</caption>
        <thead>
            <tr>
                <th scope="col">Name</th>
                <th scope="col">Department</th>
                <th scope="col">Action</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var employee in Visible)
            {
                <tr @key="employee.Id">
                    <td>@employee.Name</td>
                    <td>@employee.Department</td>
                    <td>
                        <button type="button" @onclick="() => Select(employee)">
                            View @employee.Name
                        </button>
                    </td>
                </tr>
            }
        </tbody>
    </table>
}

@if (selected is not null)
{
    <p role="status">
        Selected @selected.Name, started @selected.StartDate.ToString("d MMM yyyy")
    </p>
}

@code {
    private readonly List<Employee> employees =
    [
        new(1, "Priya Nair", "Finance", new DateOnly(2019, 4, 8)),
        new(2, "Tom Whelan", "Operations", new DateOnly(2021, 9, 20)),
        new(3, "Sara Kowalski", "Engineering", new DateOnly(2023, 1, 16)),
    ];

    private string searchTerm = string.Empty;
    private Employee? selected;

    private List<Employee> Visible => employees
        .Where(e => e.Name.Contains(searchTerm, StringComparison.OrdinalIgnoreCase))
        .ToList();

    private void SearchChanged(ChangeEventArgs args)
    {
        searchTerm = args.Value?.ToString() ?? string.Empty;
    }

    private void Select(Employee employee) => selected = employee;
}
  • Employee is a record declared in your models: public record Employee(int Id, string Name, string Department, DateOnly StartDate). The list is built inline here so the example stands alone; a real screen loads it from a service.
  • @if and @foreach are C# control flow in markup. The braces are C# braces, and the markup inside them is what gets produced for each pass. There is no separate template language to learn.
  • value="@searchTerm" writes the current term into the input, and @oninput calls SearchChanged on every keystroke. ChangeEventArgs carries the new value as an object, so it needs converting. The next lesson replaces this pair with @bind, which is shorthand for precisely this.
  • @onclick="() => Select(employee)" passes a lambda, which is how you get the loop variable into the handler. Note that it is a lambda and not a call — writing Select(employee) without the arrow would invoke the method during rendering.
  • @key="employee.Id" tells Blazor which rendered row belongs to which employee. When the list is filtered or reordered, it moves the existing rows instead of rewriting each one in place.
  • Visible is a property, so it is recalculated every time the component renders. Filtering three records in memory is nothing; if this property called an API, it would call it on every render. See the note at the end of this lesson.
  • The button text includes the employee name so that each button has a distinct accessible name. A column of buttons all called View is ambiguous to anyone reading them out of context. If the design wants only the word View on screen, put the name in a visually hidden span inside the button.
  • scope="col" on the header cells, and the table caption, are what make the table navigable with a screen reader. Both are content decisions, so they belong here rather than in CSS.

Why the screen updates without you updating it

Clicking View runs six steps, and only one of them is code you wrote:

  1. Blazor renders the component

    It evaluates the markup and builds a tree describing the elements, attributes and text that should exist. This tree is Blazor's own structure, not the browser's.

  2. The browser shows the result

    On a first render, Blazor produces the HTML for that tree and the browser displays it as usual.

  3. Someone clicks the button

    The click is captured and routed to the handler you named in @onclick. In server mode it travels over the SignalR connection; in WebAssembly it stays in the browser.

  4. Your handler runs

    Select assigns to the selected field. That is all your code does. It does not touch the page, and it does not look for the paragraph it wants to change.

  5. Blazor renders the component again

    When the handler finishes, the framework re-renders it and builds a fresh tree, now including the paragraph about the selected employee.

  6. Only the differences are applied

    The new tree is compared with the previous one and the browser is told about the changes alone. Untouched rows are left where they are, which is why scroll position and focus survive.

That automatic re-render happens after any event handler the framework invoked. It is the reason a Blazor component has no update-the-page code in it, and the reason state and display cannot get out of step within a component.

Two details are worth knowing early. An async handler renders more than once: Blazor renders when the handler first awaits, and again when it completes, so a loading state is easy to show. And state that changes outside an event handler — a timer firing, a message arriving from a background service — does not trigger anything, because Blazor never knew about it. In that case you call StateHasChanged yourself to ask for a render.

If you find yourself calling StateHasChanged inside a normal event handler, that is a sign something else is wrong. The framework already rendered for you.

The markup constructs used above, collected in one place:

@expression
Writes the value of a C# expression into the markup. Wrap anything with spaces or operators in brackets: @(count + 1). Output is HTML-encoded, so text cannot break the page or inject markup.
@if, @else, @switch
Conditional markup. Only the branch that matches is rendered — the other branch produces no elements at all, rather than hidden ones.
@foreach, @for, @while
Repeated markup, one pass per item, with the loop variable available to the markup inside.
@key
Identifies a repeated element by something stable, usually an id. It changes how Blazor matches old output to new when a list changes order or length.
@onclick, @oninput, @onchange, @onsubmit
Event handlers. The value is a method or a lambda. Handlers can be void, or return Task when they need to await something.
@@ and @* *@
Two @ characters produce a literal @ — needed for an email address in markup. A comment between @* and *@ is removed at compile time and never reaches the browser.

Summary

  • A component is markup with C# expressions plus a @code block holding fields, properties and methods
  • @if, @foreach and @ expressions are C# in markup, so there is no separate template language
  • Event handlers named with @onclick and friends are ordinary methods in the same file
  • Blazor re-renders the component after a handler runs and applies only the differences, which is why you never write update code
  • State changed outside an event — a timer, a background service — needs StateHasChanged, and repeated elements that reorder need @key

Practice

Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.

Try it yourself

Add a department filter

Add a select element listing the departments in the data, and combine it with the existing search box so both apply at once. Include an option that clears the filter.

Notice how much of the update logic you have to write for the table to reflect the new filter.

Show solution

None of it. You add a field, extend the filter in Visible, and handle the change event. The table updates because the component re-renders after the handler, and the markup is a description of the current state rather than a set of instructions for changing the page.

Deriving the department list from the data rather than hard-coding it means a new department appears in the filter without a second edit. It also means the filter cannot offer a department nobody works in.

Keep the empty option's value distinct from a real department and label it clearly. A blank option with no text is a control that a screen reader announces as nothing at all.

C#
<label for="department">Department</label>
<select id="department" value="@department" @onchange="DepartmentChanged">
    <option value="">All departments</option>
    @foreach (var name in Departments)
    {
        <option value="@name">@name</option>
    }
</select>

@code {
    private string department = string.Empty;

    private IEnumerable<string> Departments => employees
        .Select(e => e.Department)
        .Distinct()
        .OrderBy(name => name);

    private List<Employee> Visible => employees
        .Where(e => e.Name.Contains(searchTerm, StringComparison.OrdinalIgnoreCase))
        .Where(e => department.Length == 0 || e.Department == department)
        .ToList();

    private void DepartmentChanged(ChangeEventArgs args)
    {
        department = args.Value?.ToString() ?? string.Empty;
    }
}

Challenge

Sort the table, and watch @key earn its place

Add a button that toggles sorting between name and start date. Before you run it, put focus in a text input inside one of the rows, or open a select in one, then sort.

Do it once with @key on the row and once without. What differs, and why does the difference only appear when the order changes?

Show solution

Without @key, Blazor matches the new output to the old by position. The first row is still the first row, so it patches the differences into the element that is already there. The element keeps its browser state, which now belongs to a different employee — focus, selection and typed text end up on the wrong row.

With @key, rows are matched by employee id. Blazor sees that the elements still exist and moves them, so element state travels with the employee it belongs to.

Nothing differs while the order is stable, which is exactly why this bug reaches production. It shows up the first time a list is sorted, filtered or has an item removed from the middle, and by then the missing @key looks unrelated to the symptom.

Knowledge check

Nothing is recorded and there is no score. The explanation appears either way.

You click a button, the handler assigns to a private field, and a paragraph showing that field updates on screen. What made the update happen?
What does @key on an element inside a @foreach do?

Saved in this browser only.