Telerik blogs
OpinionT Light_870x220

Should we rewrite native UI components or reuse existing JavaScript UI components? We compare native Blazor components to wrapped JavaScript components by understanding how Blazor works, including what native vs. interop means and more.

Blazor is a new Single Page Application (SPA) framework that utilizes WebAssembly to run .NET application code in the browser. This future-forward framework allows developers to leverage their existing .NET code and skills to create web applications that can run completely client-side without the need for browser plug-ins. As with any new web framework, we're presented with a challenging decision of bringing along assets from previous works into the new system.

In the case of Blazor, the challenge presents itself in the User Interface (UI) components of the application model. Due to previous lack of choice, web UIs are written in JavaScript, whereas Blazor heavily utilizes the C# language and Razor markup syntax. This stark contrast of choices forces one's hand in one of two directions - rewrite native UI components or reuse your JavaScript UI components.

Let's explore the challenge by understanding how Blazor works, what native vs. interop means, all while discussing the tradeoffs of choosing between the two approaches.

Blazor Architecture Basics

Blazor is a new breed of web application framework, a first of its kind. Blazor is similar in many respects to React or Angular, but what sets it apart is the underlying architecture is crafted upon WebAssembly instead of JavaScript.

WebAssembly (WASM) is a web standard, developed by the World Wide Web Consortium (W3C), that defines an assembly-like binary code format for execution in web pages. WebAssembly is the cornerstone of Blazor in that it provides a platform for the Mono Runtime for WebAssembly, a .NET runtime compiled to WASM format.

A Blazor application is a true .NET application, running on a .NET runtime inside the browser. This is quite an important factor in the decision process when it comes to writing UI components for Blazor, as you should know the context in which the component is executed.

Blazor Architecture: DOM Abstraction

Much like its JavaScript siblings, Angular and React, Blazor employs a similar approach to handling changes to the Document Object Model (DOM). No matter what framework you choose, DOM manipulation is a taxing process which is often compounded by directly changing its structure more frequently than necessary. Without a proper execution plan, most approaches to DOM manipulation destroy and rebuild chunks of DOM, ripping out multiple nodes and repainting them. This is a solved problem in modern frameworks through the use of a DOM abstraction.

The DOM abstraction in Blazor is called the RenderTree, it's a lightweight representation of the DOM. Think of the RenderTree as copy where changes can be quickly made as nodes in this tree can be created, updated and deleted without consequence of re-rendering the page. Now multiple components in the system can make changes against the RenderTree at once with much less of a performance hit. When the dust has settled, the RenderTree and DOM are reconciled by looking for the differences between the two and re-rendering only what's absolutely necessary.

The RenderTree is vital to UI components behavior and render speed. It's also an important aspect to choosing between how to write UI components, especially when it comes to JavaScript.

Blazor Architecture: Native Components

In a Blazor application, components (.razor) are actually processed quite differently from traditional Razor (.cshtml) markup. Razor in the context of MVC or Razor Pages is processed directly to HTML, which is rendered server side and sent over an HTTP request. A component in Blazor takes a different approach - its markup is used to generate a .NET class that builds the RenderTree.

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" onclick="@IncrementCount">Click me</button>

Each HTML element in the component gets passed to the RenderTreeBuilder and given a sequence number used to quickly differentiate changes in the DOM.

    public class Counter : ComponentBase
    {
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            base.BuildRenderTree(builder);
            builder.AddMarkupContent(0, "<h1>Counter</h1>\r\n\r\n");
            builder.OpenElement(1, "p");
            builder.AddContent(2, "Current count: ");
            builder.AddContent(3, currentCount);
            builder.CloseElement();
            builder.AddContent(4, "\r\n\r\n");
            builder.OpenElement(5, "button");
            builder.AddAttribute(6, "class", "btn btn-primary");
            builder.AddAttribute(7, "onclick", M...<UIMouseEventArgs>(this, IncrementCount));
            builder.AddContent(8, "Click me");
            builder.CloseElement();
        }

This component architecture is fundamental to Blazor's operation and supports features built into the framework such as: component life cycle methods, templates and validation.

Blazor Architecture: JavaScript Wrappers

A "native" Blazor component is one that is written using the framework's component architecture, a component that has a RenderTree and hooks into life cycle methods. The alternative is to create a wrapper for preexisting JavaScript components. When using JavaScript a component is created that exposes a set of properties and methods that map to a JavaScript implementation. On the surface a JavaScript based component is used like native component, but under the surface it bypasses the RenderTree and the HTML is rendered by directly manipulating the DOM.

The Blazor RenderTree process vs. direct DOM manipulation. RenderTree consolidates updates, takes the delta, performs a single update. JavaScript updates even when it is not necessary.

The ability to perform calls to JavaScript from within a Blazor application is called the JavaScript interop. The interop is a necessary feature of Blazor as it bridges any gaps between WebAssembly and DOM APIs. This is especially useful for things not supported in Blazor like: GeoLocation, File Access, and Camera APIs to name a few. It's a very powerful tool, but it can easily become bad practice as it can circumvent the Blazor virtual DOM.

Leaky Abstractions

In software development, a leaky abstraction is an abstraction that leaks details that it is supposed to abstract away. Wrappers have a natural tendency to fall victim to this definition. Because wrappers need support from outside the Blazor framework, they require additional details like id and ref attributes. Legacy JavaScript code relies heavily on ids and elementRefs. Since they exist outside of the RenderTree, they need to be located in the DOM by traversing the DOM structure and then manipulated, a costly routine. If these are required attributes for a UI component library you can assume its heavily dependent on JavaScript.

<AcmeWidget ID="myThing" ref="myReference" ...>

Content & Template Support

Components are the building blocks of Blazor application and one of its strongest assets. Components can host other components in two ways - through Child Components and Templates - and as a result Blazor development is very nimble. Child Components and Templates are component properties with a special class called a RenderFragment, a delegate that writes the content to a RenderTreeBuilder. This is another oversight for components built from JavaScript wrappers and a prime reason to build native components.

Child components are extremely powerful as they can transform component behavior or composition of components. Such a feature can truly be appreciated by example. In the following code a TelerikTabStrip is given a list of Forecast objects, the list is iterated over using a simple foreach, building the tabs dynamically.

<TelerikTabStrip>
    @foreach (var f in Forecasts)
    {
        <TelerikTab Title=@f.City>
            <Weather City=@f.City
                     TempC=@f.Temp
                     Forecast=@f.Outlook>
            </Weather>
        </TelerikTab>
    }
</TelerikTabStrip>

The ability to declare components in this manner is due to the Blazor framework's component architecture and use of RenderFragments.

Dynamic components aren't limited to simple foreach loops either. Virtually any C# methodology can be applied to control rendering. In the next example, the same TelerikTabStrip component is used with an if statement which is bound to a check-box embedded within one of the child tabs. Changing the value of the check-box in this instance instantly effects the visibility of the first tab.

<TelerikTabStrip>
    @if (showFirstTab)
    {
        <TelerikTab Title="First Tab">
            This is the first tab.
        </TelerikTab>
    }
    <TelerikTab Title="Second Tab">
        <label>
            <input type="checkbox" bind=@showFirstTab />
            Toggle first tab
        </label>
    </TelerikTab>
</TelerikTabStrip>

This is possible because the scope of showFirstTab is outside of the component itself. Since the components are native they obey Blazor's rendering and data binding capabilities.

More advanced scenarios play out when Templates are used to allow for customization of how a component renders markup. Templates can be used for simple tasks like formatting values or displaying images, while more extensive Templates can transform the user interface completely, adding entirely new functionality to a component. For further reading on this subject, the article "Why Blazor Grid Templates Will Make You Question Everything" gives a comprehensive overview with code examples.

Pre-Rendering

Blazor isn't just capable of running client side, it can be hosted server-side as well. When Blazor runs server-side it has the ability to pre-render views to eliminate loading screens and enhance SEO. The way pre-rendering works in Blazor is similar to how ASP.NET Razor views and pages are rendered. In fact, because of this Blazor components are compatible with ASP.NET Razor views and pages. When using JavaScript based components, the server can not pre-render the component as JavaScript is not yet available. Instead, the page will need to be rendered by JavaScript when it is parsed by the client. It's important to be aware that this mode of operation is unavailable when wrapping existing JavaScript components.

Validation

Validation in Blazor is tightly coupled with the core component system. Since Blazor uses C# throughout the programming model, it supports the popular DataAnnotation method of defining validation.

using System.ComponentModel.DataAnnotations;

public class ExampleModel
{
    [Required]
    [StringLength(10, ErrorMessage = "Name is too long.")]
    public string Name { get; set; }
}

The DataAnnotations can be used to trigger validation within business logic on the server and also UI validation. The built-in EditForm component provides validation and event handlers that provide a seamless work-flow in the component model.

<EditForm Model="@exampleModel" OnValidSubmit="@HandleValidSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <InputText id="name" bind-Value="@exampleModel.Name" />

    <button type="submit">Submit</button>
</EditForm>

@functions {
    private ExampleModel exampleModel = new ExampleModel();

    private void HandleValidSubmit()
    {
        Console.WriteLine("OnValidSubmit");
    }
}

Once again this is a scenario that plays out best when components are built using native components. Native components share a state called an EditContext that allows components within an EditForm to communicate. Native components that have integrated the EditContext automatically validate when placed within an EditForm without additional code. When trying to wrap existing JavaScript components validation requires leaky abstractions through the use of Id and ElementRef attributes.

Is JavaScript all Bad?

There is absolutely nothing wrong with using JavaScript libraries to support Blazor components. There are some use cases where JavaScript interop is a necessity to fill gaps as WebAssembly and Blazor are maturing products. One way that JavaScript interop can be beneficial is when it is used to augment native components by adding features that do not disturb the RenderTree, such as giving focus or providing keyboard navigation. In addition, JavaScript libraries can be used to add system level functionality when tapping into the DOM Web APIs like GeoLocation, File access, Canvas, and SVG.

Telerik is Native First

With Telerik UI for Blazor we have taken a native first development approach to building UI components. Our best practice is to take native implementation as far as the framework allows, leveraging the RenderTree, and allowing components to naturally hook into templates and validation. Our components make minimal use of JavaScript interop, ensuring that there are no leaky abstractions filtering up to the surface and getting in the customer's way. We have found extensive benefits with templates that offer an open-ended approach to web building applications.

Our Kendo UI brand of JavaScript components are a substantial asset to us and our customers, however we have determined that the best approach is to rebuild for Blazor with the knowledge we have gained from building UI components for Kendo UI. We can then apply it to Blazor using its fully native technologies, rather than wrapping the JavaScript work and passing it along to customers as something new.


About the Author

Ed Charbeneau

Ed Charbeneau is a web enthusiast, speaker, writer, design admirer, and Developer Advocate for Telerik. He has designed and developed web based applications for business, manufacturing, systems integration as well as customer facing websites. Ed enjoys geeking out to cool new tech, brainstorming about future technology, and admiring great design. Ed's latest projects can be found on GitHub.

Related Posts

Comments

Comments are disabled in preview mode.