This content originally appeared on Telerik Blogs and was authored by Jon Hilton
Even if you start your UI all in one place, here’s how to break it down into building blocks for clean, scalable Blazor code.
If you’re coming from MVC or Razor Pages, you might be tempted to treat your Razor components (in a Blazor app) as pages. If so, you’ll likely end up with a few big components, with lots of UI and UI logic in one place.
But if you do, you’re missing out on a lot of Blazor’s potential. Blazor (and other component-based frameworks) come into their own when you break your UI down into smaller pieces and compose them together to form larger features. Here’s how to break your UI down into these smaller building blocks.
Start with the UI in One Place
It might actually make sense to start with the UI in one component. If you’re trying to recreate a design/mockup or just break ground on a new feature, it’s easier to throw HTML and CSS around when it’s all in one place.
You can quickly iterate the UI, tweak its appearance and use dummy text/hardcoded data to get something up and running in the browser.
But then, as your feature takes shape, it pays to start breaking it down into smaller pieces.
Building Blocks
Take this example of a simple product page for an online store.
At this point, this is fairly manageable. But let’s say we need to create a new “wishlist” page, which shows the specific products a customer has favorited/added to a wishlist.
We could just copy and paste the markup into a new page, marvel at our success and call it a day…
But now we’re storing up trouble.
As new requirements emerge, or we need to tweak the appearance of product cards, we’re going to have to remember to update the code in two places.
This is the perfect time to do a spot of refactoring.
If we extract the product card UI into its own component, we can use it in both pages.
ProductCard.razor
<div class="card">
<img class="card-img-top" src="@Product.Image" alt="Product Image Description"/>
<div class="card-body">
<h5 class="card-title">@Product.Title</h5>
<p class="card-text">@Product.Price.ToString("C")</p>
</div>
<div class="card-footer">
<p class="text-center">FREE Delivery</p>
</div>
</div>
@code {
[Parameter]
public ProductSummary Product { get; set; }
}
Now each page can render instances of this ProductCard
component instead of duplicating it every time.
Wishlist.razor
@page "/Wishlist"
<section class="container my-4">
<h2>Your Wishlist</h2>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4 mt-2">
@foreach (var product in products)
{
<div class="col">
<ProductCard Product="@product"/>
</div>
}
</div>
</section>
@code {
private List<ProductSummary> products;
protected override Task OnInitializedAsync()
{
products = ProductStore.GetWishlist();
return base.OnInitializedAsync();
}
}
But let’s say we want to go a step further.
Reusable Building Blocks
A new requirement comes in, and we realize we want that same card style UI, but for something that isn’t actually a product. We like the card UI and want to reuse it, but now we’re talking about an entirely different use case.
Our ProductCard
is currently hardwired to deal with products (it takes the product details as a parameter). So how to reuse it for a completely different context?
The answer is to take the card UI markup from the ProductCard
UI and extract it into its own Card
component.
Card.razor
<div class="card" style="width: 18rem;">
<img class="card-img-top" src="@ImageUrl" alt="Product Image Description"/>
<div class="card-body">
<h5 class="card-title">@Title</h5>
<p class="card-text">@Text</p>
</div>
<div class="card-footer text-center">
@FooterContent
</div>
</div>
@code {
[Parameter] public string ImageUrl { get; set; }
[Parameter] public string ImageAltText { get; set; }
[Parameter] public string Title { get; set; }
[Parameter] public string Text { get; set; }
[Parameter] public RenderFragment FooterContent { get; set; }
}
We’re essentially creating a Card
abstraction, so we can give this whichever parameters make sense for a generic re-usable card.
Notice how we’ve gone from the specific to more general here.
Product List > Product Card > Card
and…
WishList > Product Card > Card.
On the left-hand side these components (and/or pages) are very specific, tied up with business logic for one part of our domain.
But as we move down the component tree (to the right) we get to components which are less specific/more general (and therefore reusable).
This use of composition—composing components together to form larger features—has a lot of benefits:
- Smaller components to reason about
- Self-contained state (each instance of a component is just concerned with its own state)
- Components can be reused in different parts of the UI
- Components don’t necessarily need to re-render when their parent does (unless their own state and/or parameters have changed)
Here’s the final version of our product list, complete with the ProductCard
and Card
components.
3 Tips for Effectively Creating Your UI Using Component Building Blocks
Here are some tips for breaking your UI down into smaller pieces.
Use Render Fragments to Render Arbitrary Content
In the Card
component, we declared a RenderFragment
parameter called FooterContent
.
Render fragments are particularly useful as they enable you to pass any arbitrary content to that parameter and have it rendered where you want in the card markup.
You can specify multiple RenderFragment
parameters and provide content for some/all of them when declaring instances of the component.
Keep Reusable Components Reusable
Try to avoid accidentally restricting where a component can be used.
For example, if the root HTML element in one of your components is an li
element, then you can only technically use that component inside of a list (ul
or ol
). It may be better to keep the li
in the parent component and put everything below it in your component.
We did this with the ProductCard
component when we avoided the temptation to bring the div
with the col
class into the ProductCard
component itself.
<div class="col">
<ProductCard Product="@product"/>
</div>
This keeps our ProductCard
nice and flexible, as it can go virtually anywhere in our markup.
Create Specialized Versions of Components
If you’re tempted to modify a component (say to introduce new behavior), consider creating a more specialized component that delegates to the original component instead.
For example, say we have a simple Button
component.
Button.razor
<button @onclick="OnClick" class="btn @Class">
@ChildContent
</button>
@code {
[Parameter] public EventCallback OnClick { get; set; }
[Parameter] public RenderFragment ChildContent { get; set; }
[Parameter] public string Class { get; set; }
}
We can use this button wherever we like, providing our own CSS classes as appropriate. Here’s an example of it in action.
Now let’s say we need a button that shows a confirmation when clicked, for example in the case of a delete operation we want to check the user definitely wants to proceed.
We could modify Button
to handle this requirement, but it’s already doing one job—and doing it efficiently!
The alternative is to wrap Button
in a new ButtonDanger
component.
ButtonDanger.razor
<Button Class="btn-danger" OnClick="PromptForConfirmation">
@ChildContent
</Button>
@code {
[Parameter] public RenderFragment ChildContent { get; set; }
[Parameter] public EventCallback OnClick { get; set; }
private void PromptForConfirmation(){
if(ShowConfirmationDialog == true){
OnClick.InvokeAsync();
}
}
}
Here we intercept the @OnClick
event and show a confirmation dialog.
If the result of showing that dialog is true
(the user confirmed it’s OK to proceed), we can invoke the original OnClick
method.
Here’s how we’d use this new ButtonDanger
component:
Home.razor
<ButtonDanger OnClick="ButtonClicked">
Delete Everything
</ButtonDanger>
@code {
void ButtonClicked(){
// perform logic
}
}
And here’s a simple example where we show the confirm UI directly in the ButtonDanger
component itself.
Notice how we didn’t make any changes to the underlying Button
component to support this new requirement for “dangerous buttons.”
This keeps the original button small and focused, so it isn’t polluted with conditional logic (to determine when a confirmation dialog should, or shouldn’t, be displayed).
In Summary: Compose Your UI from Small Building Blocks
When building your UI, try to refactor toward smaller, focused components. Components are at their best (and easiest to understand) when they do one thing well.
Rather than modify existing components, consider wrapping them in more specialized versions that add extra behavior/logic.
That way you can get all the benefits of building up a bank of reusable components as your UI grows and evolves.
This content originally appeared on Telerik Blogs and was authored by Jon Hilton
Jon Hilton | Sciencx (2024-07-09T15:18:57+00:00) Component Composition: The Secret to Scalable and Maintainable Blazor UI. Retrieved from https://www.scien.cx/2024/07/09/component-composition-the-secret-to-scalable-and-maintainable-blazor-ui/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.