
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 rendering, attribute splatting, named cascading values, templated components, performance 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.
A 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 theOnNameChanged
method should run immediately after theName
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 likeplaceholder
,aria-*
,style
,data-*
, 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 Canvas, WebRTC, or WebSockets
- Calling JavaScript from C# and vice versa (using
DotNetObjectReference
) - Integrating libraries like Chart.js, Leaflet, 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 architecture, performance, or flexibility. As you go deeper into component-driven development, features like RenderFragment<T>
, chained data binding, named cascading values, and dynamic rendering become essential tools for creating robust, scalable 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 success, maintainability, and performance. Happy coding!