Why Component-Based UI
By the end of this lesson
Describe what components give you over page-level markup.
A component is one file holding three things that usually end up apart: the markup for a piece of interface, the data that piece needs, and the code that runs when someone interacts with it.
Bundling them is the whole idea. If you want to know why the employee row shows a grey badge, there is one file to open. If you want to change it, there is one file to change, and the parameters at the bottom of it tell you what a caller is allowed to affect.
The alternative is page-level markup: one template per screen, shared fragments copied or pulled in as includes, and the behaviour in a script somewhere else. It works. It stops working well at about the point where two screens need to show the same thing with a small difference.
<span class="badge @CssClass">@Label</span>
@code {
[Parameter]
public EmploymentStatus Status { get; set; }
private string Label => Status switch
{
EmploymentStatus.Active => "Active",
EmploymentStatus.OnLeave => "On leave",
EmploymentStatus.Leaver => "Left the company",
_ => "Status unknown",
};
private string CssClass => Status switch
{
EmploymentStatus.Active => "badge-active",
EmploymentStatus.OnLeave => "badge-leave",
_ => "badge-muted",
};
}- The markup is one line, because one line is all this component is responsible for.
- [Parameter] marks a property a caller can set. The next module covers it properly; for now, read it as this component's input. Status is an enum declared in your models — Active, OnLeave, Leaver.
- Label and CssClass are worked out inside the component. No caller has to know which wording or which class goes with which status, and therefore no caller can get that pairing wrong.
- Every screen that shows a status uses this component: the list, the detail view, the search results. The wording and the colours cannot drift apart between them, because there is only one copy.
- The badge carries its meaning in text as well as colour. Colour alone leaves out anyone with a colour vision deficiency, anyone in a high-contrast mode, and anyone reading the page with a screen reader.
The same badge, handled two ways, and what each way costs you later:
| Page-level markup | Components | |
|---|---|---|
| Where the badge lives | Copied into every screen that shows one, or pulled in as a shared fragment with the status-to-class logic still outside it | One file that owns both the markup and the rule |
| Changing the wording | Find every copy. Missing one is silent, because the page still renders | One edit, everywhere it appears |
| Reading it | Markup in one file, behaviour in a script, the state that drives it somewhere else again | One file, top to bottom |
| Reusing it with a difference | Copy and edit. The two versions drift apart from that day on | Add a parameter |
| Testing it | Usually needs the whole page rendered, with whatever that page depends on | Render the one component with a value and assert on the output |
| Knowing what a change affects | A shared fragment can change screens you never opened | The parameters are the contract, so the compiler helps you find every caller |
What that buys you, in the order the benefits tend to show up on a real project:
- Encapsulation: the state a component needs can be private to it. Nothing outside can reach in and change it, so when the badge misbehaves there is exactly one place to look.
- Reuse across screens: the same component in the list, the detail view and the search results, with no copies to keep in step.
- A visible contract: the parameters are the component's public surface. Reading them tells you what it needs and what it announces. A page template tells you nothing of the kind.
- Testable in isolation: construct the component with a value and check the rendered output. No navigation, no database, no page.
- Bounded change: when a badge is wrong, the fix is in the badge. You are not searching the solution for other places that happen to render one.
Summary
- A component owns its markup, its state and its behaviour in one file, so there is one place to look and one place to change
- Reuse comes from parameters rather than copies, which stops two screens drifting apart
- A component can be rendered and tested on its own, without the page it normally sits in
- The parameter list is a visible contract: it says what the component needs and what it announces
- The costs are real — more files, more indirection, and an explicit decision about who owns shared state
Practice
Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.
Think about it
Where are the components?
The employee list screen has: a header with the company name and a sign-out link, a search box, a table where each row shows name, department, an employment status badge and a View button, and a footer with a support address.
Which of those should become components, and which should stay as markup on the page? Give a reason for each decision rather than a rule.
Show solution
The header and footer belong in a layout rather than in a component the page renders. They are identical on every screen and the page should not have to think about them. Layouts are covered later in this course, and they are components themselves.
The status badge is the clearest candidate: it appears on several screens and it owns a rule. The View button is not, on its own — it is a button element with a handler, and wrapping it adds a file without removing a decision.
The row is a judgement call. Extract it when the parent file has grown uncomfortable to read, or when the row starts owning behaviour of its own. The table itself appears once, on this screen, so leaving it as page markup is defensible and often better.
The search box depends on whether it holds state. If it keeps a term and announces changes, that is a component with a clear contract. If it is an input bound to a field on the page, extracting it moves the field away from the code that uses it, which makes the screen harder to follow rather than easier.
There is no single right answer here, and anyone who tells you the row must be a component is applying a rule rather than a reason.
Try it yourself
Extract a summary component
Two screens render an employee's name, department and status badge together, with slightly different markup on each.
Write an EmployeeSummary component that takes the employee and renders all three, and use it in both places. Then decide what should happen when the detail screen wants the department shown as a link and the list does not.
Show solution
The component takes the whole employee rather than three separate strings. One parameter is less to keep in step, and adding a field later does not change every caller.
For the difference, a boolean parameter such as LinkDepartment is the smallest honest answer while there are two cases. Boolean parameters multiply badly, though — three of them give eight combinations you have not tested.
When the variations pass two, stop adding flags and let the caller supply the markup instead, using a fragment parameter. Which to choose depends on how many variations you actually have, not on which technique sounds more sophisticated.
<div class="employee-summary">
<span class="employee-name">@Employee.Name</span>
@if (LinkDepartment)
{
<a href="/departments/@Employee.Department">@Employee.Department</a>
}
else
{
<span>@Employee.Department</span>
}
<EmploymentBadge Status="Employee.Status" />
</div>
@code {
[Parameter, EditorRequired]
public Employee Employee { get; set; } = default!;
[Parameter]
public bool LinkDepartment { get; set; }
}Saved in this browser only.