Advanced Blazor Component Techniques for Clean, Scalable UI Development

Blazor empowers developers to build rich, interactive web applications using C# and Razor. While it’s easy to get started, real power lies in its advanced component features. Whether you’re building a modular design system, a data-driven dashboard, or a component library, understanding and applying these techniques can take your Blazor apps from good to exceptional.

Lets walk through some advanced patterns like dynamic renderingattribute splattingnamed cascading valuestemplated componentsperformance tuning, as well as reasons for choosing class-based components over code-behind pages.

1. Component Architecture Foundations

Blazor makes it easy to split component markup and logic by using @code {} blocks or a partial class in a .razor.cs file. While this separation of files is helpful for readability, it doesn’t always support true separation of concerns.

Partial Class

In this example, the .razor file contains both the markup (HTML) and a code block (@code { ... }) together. The class behind the component is implicitly partial, allowing you to split the component’s code between markup and logic if you want.

Blazor components use partial classes because the component’s generated class is split across different files: the .razor file defines the UI and some code, while additional logic can live elsewhere. The keyword partial enables this split.

This section shows a Blazor component defined with inline code in the .razor file. It uses a partial class implicitly, combining HTML markup with component logic in a single file.

<!-- MyComponent.razor -->
<h3>@Title</h3>

@code {
[Parameter]
public string Title { get; set; }
}

Code Behind

In this approach, the logic is moved into a separate .cs file (code-behind file). The .razor file typically contains only markup, and the .cs file contains the C# logic. Both files are linked through the partial keyword, sharing the same class.

Splitting markup and logic improves maintainability and readability, especially for complex components. It allows UI designers and developers to work separately and keeps the .razor file cleaner.

This section demonstrates the code-behind pattern, where the component’s logic is separated into a .cs file. The component class is marked as partial to connect the code-behind logic with the markup defined in the .razor file.

// MyComponent.razor.cs
public partial class MyComponent
{
protected override void OnInitialized()
{
// Some business logic
}
}

Inheriting ComponentBase

However, for more advanced scenarios and cleaner architectural boundaries, inheriting from ComponentBase is a better choice.

  • Encapsulation: Logic can live in a clean, testable C# class without Razor dependencies.
  • Testability: Easier to write unit tests against ComponentBase classes than .razor.cs partials.
  • Reusability: Component logic becomes modular and reusable across components or apps.
  • SOLID Principles: Especially the Single Responsibility Principle — markup and logic are no longer tangled.

This class defines the component’s logic by inheriting from ComponentBase. It declares a parameter property (Title) and overrides the OnInitialized lifecycle method to include initialization logic. This class centralizes the component’s behavior, keeping code organized and reusable.

public class MyComponentLogic : ComponentBase
{
[Parameter] public string Title { get; set; }

protected override void OnInitialized()
{
// Centralized logic
}
}

This Razor component inherits from the MyComponentLogic class, allowing it to reuse the logic defined there. The markup renders an <h3> element that displays the Title property. This pattern cleanly separates UI markup from the component’s logic by using inheritance.

<!-- MyComponent.razor -->
@inherits MyComponentLogic

<h3>@Title</h3>

2. Templated Components and RenderFragments

Blazor supports powerful templating capabilities through RenderFragment and RenderFragment<T>. These allow developers to create highly flexible components that accept other content as input — much like slots in other frameworks.

RenderFragment & RenderFragment<T>

RenderFragment is a delegate type that represents a chunk of UI content that can be rendered. Essentially, it’s a way to pass a block of Razor markup or UI content as a parameter to a component.

RenderFragment is like a reusable snippet of UI you can pass around and render wherever you want in your component tree.

<!-- Card.razor -->
<div class="card">
<div class="card-header">@Header</div>
<div class="card-body">@ChildContent</div>
</div>

@code {
[Parameter] public RenderFragment Header { get; set; }
[Parameter] public RenderFragment ChildContent { get; set; }
}

Usage

<Card>
<Header>
<h3>Custom Header</h3>
</Header>
<p>This is the card body.</p>
</Card>

RenderFragment<T>

RenderFragment<T> is a generic version of RenderFragment. It represents a chunk of UI content that depends on a parameter of type T. The component can pass data to this fragment, which then renders UI based on that data.

It allows you to create templates that can render differently depending on the input data you provide — very useful for things like templated lists, grids, or complex dynamic UI.

<!-- ListRenderer.razor -->
@foreach (var item in Items)
{
@ItemTemplate(item)
}

@code {
[Parameter] public IEnumerable<TItem> Items { get; set; }
[Parameter] public RenderFragment<TItem> ItemTemplate { get; set; }
}

Usage

<ListRenderer Items="myData">
<ItemTemplate Context="item">
<li>@item.Name</li>
</ItemTemplate>
</ListRenderer>

3. Generic Templated Components with Constraints

This section demonstrates how to create reusable, strongly-typed Blazor components using generics with constraints. By defining a generic type parameter TItem constrained to an interface (e.g., IEntity), you can build flexible UI components—like a data grid—that work seamlessly with any data type implementing that interface. This pattern enhances code reuse, type safety, and maintainability across your application.

This generic Blazor component defines a type parameter TItem constrained to types implementing the IEntity interface. It accepts a collection of Items and a RowTemplate—a RenderFragment<TItem> delegate used to render each row dynamically. The component iterates over the Items and renders each item using the provided template, enabling a flexible and reusable data grid UI for any entity type.

<!-- GridComponent.razor -->
@typeparam TItem where TItem : IEntity

<table>
<thead>...</thead>
<tbody>
@foreach (var row in Items)
{
<tr>
<td>@RowTemplate(row)</td>
</tr>
}
</tbody>
</table>

@code {
[Parameter] public IEnumerable<TItem> Items { get; set; }
[Parameter] public RenderFragment<TItem> RowTemplate { get; set; }
}

4. Parameter Binding and Chained Binds

Blazor’s binding system simplifies two-way data flow between parent and child components. For more advanced scenarios, it provides mechanisms to finely control how data is passed, updated, and synchronized across component boundaries.

Chained Binds (Manual Two-Way Binding)

Blazor doesn’t support automatic chained @bind across multiple component layers out-of-the-box. However, you can manually implement two-way binding by combining [Parameter] properties with EventCallback<T>. This pattern enables seamless data flow and synchronization from a parent component to a child and even a grandchild component, maintaining consistent state throughout the hierarchy.

By exposing both Value and ValueChanged, this component enables manual two-way data binding. When used inside a parent that handles these parameters properly, it allows chaining bindings across multiple component levels.

<!-- MyInput.razor -->
<input @bind="Value" />

@code {
[Parameter] public string Value { get; set; }
[Parameter] public EventCallback<string> ValueChanged { get; set; }

private async Task OnValueChanged(ChangeEventArgs e)
{
Value = e.Value.ToString();
await ValueChanged.InvokeAsync(Value);
}
}

Usage

<MyInput @bind-Value="ParentValue" />

Custom @bind:after (Blazor 8+)

Starting with Blazor 8, the @bind:after directive lets you run custom logic immediately after a two-way binding update completes. This is especially useful for tasks like validation, logging, or triggering side effects right after the model’s value changes.

  • This example binds the input’s value to the Name field using @bind.
  • Custom After-Bind Action:
    The @bind:after="OnNameChanged" directive specifies that the OnNameChanged method should run immediately after the Name value updates from the input.
  • Use case:
    This lets you react to value changes outside of the typical binding flow — for example, to validate the new input, update other UI elements, or log changes without cluttering the main binding logic.
<input @bind="Name" @bind:after="OnNameChanged" />

@code {
private string Name;
private void OnNameChanged() => Console.WriteLine($"Name changed: {Name}");
}

5. Named Cascading Values

When multiple cascading values are provided within the same component hierarchy — such as authentication info and theme settings — using named cascading values helps avoid conflicts and ambiguity. By assigning explicit names, you ensure that components receive the correct cascading data. This improves component reusability across different applications with varying cascading contexts and prevents accidental value mismatches.

  • Prevents accidental mismatches
  • Makes components more reusable across apps with different cascading contexts
<CascadingValue Name="Theme" Value="dark">
<CascadingValue Name="User" Value="currentUser">
<ChildComponent />
</CascadingValue>
</CascadingValue>

Then in the child:

@CascadingParameter(Name = "Theme")
public string Theme { get; set; }

@CascadingParameter(Name = "User")
public User CurrentUser { get; set; }

6. Attribute Splatting and Arbitrary Parameters

Attribute splatting lets a Blazor component capture and pass along any extra HTML attributes that aren’t explicitly defined as parameters. This enables custom components to behave like native HTML elements by accepting arbitrary attributes seamlessly. It enhances flexibility and makes components more adaptable and easier to integrate with standard HTML behaviors.

Use Case: Wrapping Native Inputs

  • The component captures all unmatched attributes passed to it into the AdditionalAttributes dictionary via the [Parameter(CaptureUnmatchedValues = true)] directive.
  • Attribute Splatting:
    The special Razor directive @attributes="AdditionalAttributes" applies all those captured attributes directly to the native <input> element.
  • Use case:
    This pattern is useful when wrapping native HTML elements to create reusable components that still allow consumers to specify arbitrary attributes like placeholderaria-*styledata-*, etc., without needing to declare each one as a formal parameter.
<!-- MyTextBox.razor -->
<input @attributes="AdditionalAttributes" class="form-control" />

@code {
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object> AdditionalAttributes { get; set; }
}

Usage:

<MyTextBox placeholder="Enter name" maxlength="20" />

7. Referencing Components with @ref and Multiple Refs

Blazor allows you to get references to component or DOM elements via @ref.

Basic @ref Usage

This example demonstrates how to use a component reference (@ref) in Blazor to interact directly with a child component instance. Here, the MyModal component is referenced by the modal field, allowing the parent component to call its Open() method programmatically, such as when showing the modal dialog.

<MyModal @ref="modal" />

@code {
private MyModal modal;

private void ShowModal() => modal.Open();
}

Multiple Refs

You can use List<T> or Dictionary<string, T> to manage multiple component refs dynamically:

This is useful for:

  • Triggering actions across children
  • Collecting state or validation results
  • Orchestrating dynamic component trees
@foreach (var item in Items)
{
<MyItemComponent @ref="RegisterRef" />
}

@code {
private List<MyItemComponent> _refs = new();

private void RegisterRef(MyItemComponent comp)
{
_refs.Add(comp);
}
}

8. <DynamicComponent> and Runtime Rendering

Blazor allows rendering components dynamically at runtime using the built-in <DynamicComponent>.

  • CMS-driven layouts where component types and props are stored in a database
  • Plugin systems (load components based on user config)
  • Reusable modals, forms, and wrappers

Basic Usage

This renders <MyCustomComponent Title="Hello World" IsVisible="true" />.

<DynamicComponent Type="@ComponentType" Parameters="@ComponentParameters" />

@code {
private Type ComponentType = typeof(MyCustomComponent);

private Dictionary<string, object> ComponentParameters = new()
{
{ "Title", "Hello World" },
{ "IsVisible", true }
};
}

Optional: Wrapping Dynamic Components with Safety

You can build a wrapper that guards rendering:

This avoids runtime errors when ComponentType is null or invalid.

@if (ComponentType != null)
{
<DynamicComponent Type="@ComponentType" Parameters="@ComponentParameters" />
}

9. Error Boundaries

Blazor provides built-in support for error boundaries to catch and handle exceptions that occur during component rendering. Wrapping components with <ErrorBoundary> isolates errors, preventing them from crashing the entire UI. You can optionally specify a fallback UI with the <ErrorContent> tag to display a friendly message or alternative content when an error occurs.

Example

This wraps ChildComponent so any rendering errors inside it are caught and don’t break the rest of the app.

<ErrorBoundary>
<ChildComponent />
</ErrorBoundary>

Fallback UI

Here, a fallback UI (<p>Something went wrong. Please refresh.</p>) is shown if DangerousComponent throws an error, improving user experience by providing graceful error handling.

<ErrorBoundary>
<ChildContent>
<DangerousComponent />
</ChildContent>
<ErrorContent>
<p>Something went wrong. Please refresh.</p>
</ErrorContent>
</ErrorBoundary>

Advanced ErrorBoundary Handling

Blazor allows you to extend the default ErrorBoundary to implement custom error handling logic. By overriding methods like OnErrorAsync, you can log errors to monitoring services, show tailored recovery UI, or execute other side effects when a component failure occurs.

  • This example shows how to override OnErrorAsync in a custom error boundary.
  • It logs the caught exception using a logging service before delegating to the base error handling behavior.
  • This pattern enables integrating error monitoring and customized responses to component errors in your Blazor app.
protected override Task OnErrorAsync(Exception ex)
{
logger.LogError(ex, "Component failed");
return base.OnErrorAsync(ex);
}

10. Performance Optimizations

Advanced Blazor applications benefit from tuning UI rendering and memory usage.

ShouldRender

In advanced Blazor applications, fine-tuning UI rendering can significantly improve performance and reduce memory usage. One effective technique is overriding the ShouldRender lifecycle method.

By default, Blazor re-renders a component whenever its state changes. However, this can lead to unnecessary re-renders, especially in complex components. Overriding ShouldRender allows you to explicitly control whether the component should re-render based on custom logic.

In this example, the component will only re-render if the _hasStateChanged flag is true, avoiding unnecessary UI updates and improving performance.

protected override bool ShouldRender()
{
return _hasStateChanged;
}

OnParametersSetAsync vs OnInitializedAsync

In Blazor components, both OnInitializedAsync and OnParametersSetAsync are lifecycle methods used for initialization logic, but they serve different purposes.

  • Use OnInitializedAsync when the component is first initialized.
  • Use OnParametersSetAsync to respond to updates in parameters—especially useful in reusable or nested components where parameters can change without recreating the component.

In this example, the component checks if the CurrentId has changed and only then reloads the data. This ensures efficiency by avoiding unnecessary reprocessing when parameters remain the same.

protected override async Task OnParametersSetAsync()
{
if (prevId != CurrentId)
{
await LoadDataAsync();
}
}

RenderMode and Streaming (Blazor SSR)

In Blazor SSR, you can choose render modes like:

  • InteractiveServer – Uses SignalR to enable interactivity via server-side rendering.
  • InteractiveWebAssembly – Downloads and runs the app on the client using WebAssembly.
  • Static – Renders HTML without interactivity (purely static content).
  • Auto (Blazor 8+) – Automatically selects the best mode based on the client environment.

These control when and how the app becomes interactive on the client side.

Theming and Styling

Blazor supports flexible styling strategies, including scoped CSS that applies styles only to the specific component.

Scoped CSS per Component

Scoped CSS per Component — Define styles in a .razor.css file named after the component to automatically scope styles locally.

This ensures style encapsulation, preventing unintended overrides across components.

/* MyComponent.razor.css */
.title {
color: red;
}
<h1 class="title">Hello</h1>

Theming with CascadingValue

Blazor allows you to pass down shared data like a theme across the component tree using cascading values.

  • Named Cascading Values — Provide a named value (e.g., Theme) that can be accessed by any descendant component.

Child components can retrieve the Theme using [CascadingParameter(Name = "Theme")], making it easy to apply consistent theming across the app.

<CascadingValue Name="Theme" Value="@CurrentTheme">
@Body
</CascadingValue>

Reusability Patterns

Advanced Blazor components benefit from design patterns that enhance maintainability, reusability, and testability.

Base Component Classes

Abstracts shared logic into reusable base classes to avoid repetition and standardize behavior.

public abstract class FormBase<TModel> : ComponentBase
{
[Parameter] public TModel Model { get; set; }

protected void NotifyChange() => StateHasChanged();
}

Composition over Inheritance

Build complex components by composing smaller components rather than relying on deep inheritance hierarchies, leading to cleaner and more modular code.

Bonus Advanced Topics

These features are often overlooked but add significant power and flexibility to your Blazor components.

Keyed Rendering with @key

Use @key in foreach loops to help Blazor track and preserve component instances more accurately during rendering.

This prevents UI glitches and state mixups by ensuring Blazor reuses or replaces components based on a stable identifier (like item.Id) during diffing.

This technique improves rendering efficiency and component state reliability, especially when the collection changes dynamically.

@foreach (var item in Items)
{
<MyItemComponent @key="item.Id" Item="item" />
}

Virtualization with <Virtualize>

For rendering large datasets efficiently, use the <Virtualize> component to load and display only visible items in the viewport.

This reduces memory usage and improves rendering performance.

Blazor automatically handles item recycling and incremental rendering as the user scrolls, making it ideal for long lists.

<Virtualize Items="@LargeList" Context="item">
<div>@item.Name</div>
</Virtualize>

Only visible items are rendered to the DOM, improving performance dramatically.

Lazy Loading with <Virtualize>

In addition to static lists, <Virtualize> supports lazy loading via an ItemsProvider callback. This is ideal for large or remote datasets.

The LoadItems method loads data on demand as the user scrolls, improving performance and reducing initial load time.

<Virtualize ItemsProvider="LoadItems" />

JavaScript Interop (JSInterop)

Use JavaScript interop to access browser APIs or third-party JavaScript libraries from your Blazor app.

Common use cases include:

  • Working with browser features like CanvasWebRTC, or WebSockets
  • Calling JavaScript from C# and vice versa (using DotNetObjectReference)
  • Integrating libraries like Chart.jsLeaflet, or Mapbox

This example shows a basic JS call (alert) from C#, demonstrating how easy it is to bridge .NET and JavaScript functionality in Blazor.

@inject IJSRuntime JS

<button @onclick="ShowAlert">Click</button>

@code {
private async Task ShowAlert()
{
await JS.InvokeVoidAsync("alert", "Hello from Blazor!");
}
}

Component Testing with bUnit

Use bUnit to write unit tests for your Razor components with ease. It supports event triggering, cascading parameters, dependency injection, and more.

This test renders MyComponent with a Title parameter and verifies the generated markup matches the expected output, ensuring your UI behaves correctly.

var ctx = new TestContext();
var cut = ctx.RenderComponent<MyComponent>(parameters => parameters
.Add(p => p.Title, "Hello"));

cut.MarkupMatches("<h1>Hello</h1>");

Clean Architecture in Blazor

While Blazor allows mixing UI and logic in .razor files using @code blocks, this approach doesn’t scale well for complex components. Applying clean architecture principles improves maintainability and testability.

Prefer Class-Based Components over Code-Behind

Using full class-based components that inherit from ComponentBase helps you achieve:

  • Better separation of concerns between UI and logic
  • Easier unit testing of component logic
  • Adherence to SOLID principles
  • Cleaner and less bloated .razor files
public class MyFormComponent : ComponentBase
{
[Parameter] public MyModel Model { get; set; }

protected override async Task OnInitializedAsync()
{
// Complex logic here
}
}

And in MyFormComponent.razor:

@inherits MyFormComponent

<form>
<input @bind="Model.Name" />
</form>

UI Layering

Structure your UI by breaking it into clear layers to improve organization and maintainability.

Use Dependency Injection (DI) to pass services and dependencies cleanly between layers, promoting loose coupling and testability.

Project Structure Example:

This mirrors common clean architecture practices while keeping the benefits of Blazor’s component-based approach.

/Components
- MyForm.razor
- MyForm.cs (inherits ComponentBase)
/Services
- IUserService.cs
- UserService.cs
/Models
- User.cs

Blazor has matured into a highly capable UI framework, allowing C# developers to build modern SPAs without sacrificing architectureperformance, or flexibility. As you go deeper into component-driven development, features like RenderFragment<T>chained data bindingnamed cascading values, and dynamic rendering become essential tools for creating robustscalable applications.

By favoring class-based components, using clean architecture, and understanding how to leverage Blazor’s advanced capabilities, you position your codebase for long-term successmaintainability, and performance. Happy coding!