Blazor from Scratch: Chapter 3 — Components That Scale

· 4 min read

Welcome to Chapter 3 of Blazor from Scratch. If you missed Chapter 2, read it first so your project setup is already in place.

In Chapter 2 we made the app run. In this chapter we make it maintainable.

Blazor apps become messy fast when every page is a giant .razor file. Components are how you avoid that. A good component system gives you consistency, reuse, and clearer boundaries between UI pieces.


What a Blazor component really is

A component is just a .razor file that can:

  • Render markup
  • Hold local state
  • Accept input via parameters
  • Raise events to parent components
  • Render child content

At runtime, Blazor treats each component as a small state machine. When state changes, Blazor re-renders and applies a DOM diff.

That means your job is to define clean inputs and predictable behavior.


Step 1: Start with a focused component

Create Components/Common/SectionHeader.razor:

<header class="section-header">
    <h2>@Title</h2>
    @if (!string.IsNullOrWhiteSpace(Subtitle))
    {
        <p>@Subtitle</p>
    }
</header>

@code {
    [Parameter] public string Title { get; set; } = string.Empty;
    [Parameter] public string? Subtitle { get; set; }
}

Use it in Components/Pages/Home.razor:

@page "/"

<PageTitle>Blazor from Scratch</PageTitle>

<SectionHeader
    Title="Blazor from Scratch"
    Subtitle="Chapter 3 is about components." />

This is tiny, but it introduces the most important idea: a component should be easy to read through its parameters alone.


Step 2: Use parameters to make behavior explicit

Let’s create a reusable button component.

Components/Common/AppButton.razor:

<button class="app-button @VariantCssClass" @onclick="OnClick">
    @Text
</button>

@code {
    [Parameter] public string Text { get; set; } = "Button";
    [Parameter] public string Variant { get; set; } = "primary";
    [Parameter] public EventCallback OnClick { get; set; }

    private string VariantCssClass => Variant.ToLowerInvariant() switch
    {
        "secondary" => "app-button--secondary",
        "danger" => "app-button--danger",
        _ => "app-button--primary"
    };
}

Use it:

@code {
    private int _savedCount;

    private void Save()
    {
        _savedCount++;
    }
}

<AppButton Text="Save" Variant="primary" OnClick="Save" />
<p>Saved @_savedCount times.</p>

The key point is not the button itself. The key point is contract design:

  • Keep parameters small and clear
  • Prefer explicit names over “magic” behavior
  • Give safe defaults

Step 3: Use RenderFragment for layout composition

RenderFragment lets parent components pass chunks of UI into child components.

Create Components/Common/Card.razor:

<article class="card">
    <header class="card__header">@Title</header>
    <section class="card__body">
        @ChildContent
    </section>
</article>

@code {
    [Parameter] public string Title { get; set; } = string.Empty;
    [Parameter] public RenderFragment? ChildContent { get; set; }
}

Use it:

<Card Title="Roadmap">
    <ul>
        <li>Components</li>
        <li>Data binding</li>
        <li>Routing</li>
    </ul>
</Card>

This pattern is one of the cleanest ways to build consistent page structure without repeating wrapper markup everywhere.


Step 4: Prefer composition over giant pages

If a page keeps growing, split it by responsibility:

  • ProfileSummary for top identity block
  • ProfileStats for metrics
  • ProfileActivityList for recent events

Then your page becomes orchestration, not implementation detail.

This usually gives you:

  • Smaller files
  • Easier reviews
  • Better testability
  • Less accidental coupling

Step 5: Keep markup and logic balanced

For simple components, inline @code is fine.

For larger components, move logic into code-behind:

  • UserCard.razor
  • UserCard.razor.cs

That keeps markup readable and makes C# logic easier to navigate.

You don’t need a strict rule on day one, but once a component mixes a lot of markup and state logic, split it.


Step 6: A practical folder structure

One structure that scales well:

  • Components/Pages/ -> routable pages
  • Components/Layout/ -> app shell and navigation
  • Components/Common/ -> shared generic building blocks
  • Components/Features/<FeatureName>/ -> feature-specific components

Example:

Components/
  Common/
    AppButton.razor
    Card.razor
    SectionHeader.razor
  Features/
    Dashboard/
      DashboardSummary.razor
      DashboardChart.razor
  Pages/
    Home.razor

The goal is not “perfect architecture”. The goal is reducing the time it takes to find and change UI safely.


Common mistakes in early Blazor projects

  • Passing too many parameters instead of creating a dedicated view model
  • Putting business rules directly in page components
  • Creating one “God component” with hundreds of lines
  • Repeating markup patterns instead of extracting small reusable pieces

If you avoid these four, your project quality jumps quickly.


Up next

In Chapter 4 we’ll focus on data binding and events: @bind, event handling, two-way binding tradeoffs, and patterns that keep state predictable.