Blazor from Scratch: Chapter 3 — Components That Scale
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:
ProfileSummaryfor top identity blockProfileStatsfor metricsProfileActivityListfor 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.razorUserCard.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 pagesComponents/Layout/-> app shell and navigationComponents/Common/-> shared generic building blocksComponents/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.