Skip to content

Idea: Stateless Blazor components #54547

Open
@egil

Description

@egil

To improve the performance of Blazor apps, the recommendation is to create reusable render fragments.

For components that qualify, the Razor compiler could create static reusable render fragments instead of regular instantiable components, that are made available as any other components for usage.

For example, the following ChatMessage.razor component:

<div class="chat-message">
    <span class="author">@Message.Author</span>
    <span class="text">@Message.Text</span>
</div>

@code {
    [Parameter]
    public ChatMessage? Message { get; set; }
}

This could be transpiled into the following:

public static class ChatMessage
{
  public readonly static RenderFragment<ChatMessage?> Fragment = builder => message =>
  {
    builder.OpenElement(0, "div");
    builder.AddAttribute(1, "class", "chat-message");

    builder.OpenElement(2, "span");
    builder.AddAttribute(3, "class", "author");
    builder.AddContent(4, message.Author);
    builder.CloseElement();

    builder.OpenElement(5, "span");
    builder.AddAttribute(6, "class", "text");
    builder.AddContent(7, message.Text);
    builder.CloseElement();

    builder.CloseElement();
  }
}

The key is that the usage of the "static ChatMessage" component should be the same as if it was a regular component, i.e.:

<ChatMessage Message=@message />

@code {
  private ChatMessage? message = ... ;
}

Benefits

Performance is the key motivator, and enabling it without having to write components in a different way than normal.

It will enable performance-cautious users to break their larger components into smaller, easier-maintained components, without impacting performance.

To be valuable, it should be seamless, meaning the user should not decide, e.g. through an attribute, whether they want a static component or a normal component, and they should be used similarly.

What components would qualify

I think it should be safe to create a static component if the following apply:

  1. Component do not have instance fields defined.
  2. Component do not have any life-cycle methods explicitly defined (i.e. constructor, SetParameterAsync, Dispose, DisposeAsync).
  3. Component do not have async code.
  4. Component can have parameters and services injected into them.

Methods, both static and instance methods, are OK, since they can be converted to local functions inside the generated render fragment.

Static fields and constants can just be included in the generated static class where the render fragment is defined, so that should be fine too.

Any parameters and/or injected services would be passed as arguments to the generated render fragment.

Since render fragments are not async, async code is not allowed, and users would have to have to do any async stuff outside and pass the resulting data in.

It may be that the compiler needs to mark the static components with an attribute or similar to mark them as static components for the editor or the compiler itself when static components are used in other components.

A more complex example:

@inject TimeProvider tp

<div class="chat-message">
  <span class="time">@ToLocal(Message.Time, tp)</span>
  <span class="author">@Message?.Author ?? "Unknown"</span>
  <span class="text">@Message?.Text ?? "No message content"</span>
  <button @onclick="LikeMessage">Like</button>
</div>

@code {
  private static int LikeMultiplier = 42;

  [Parameter]
  public ChatMessage? Message { get; set; }

  [Parameter]
  public EventCallback<int> OnLike { get; set; }

  private Task LikeMessage()
  {
    var like = Message.Likes + LikeMultiplier;
    return OnLike.InvokeAsync(like);
  }

  private static DateTimeOffset ToLocal(DateTimeOffset time, TimeProvider timeProvider) 
    => TimeZoneInfo.ConvertTime(time, timeProvider.LocalTimeZone)
}

Would be compiled into the following:

using Microsoft.AspNetCore.Components;
using System.Threading.Tasks;

public static class ChatMessage
{
  private static readonly int LikeMultiplier = 42;

  public readonly static RenderFragment<ChatMessage?, EventCallback<int>, TimeProvider> Fragment = builder => (message, onLike, tp) =>
  {
    builder.OpenElement(0, "div");
    builder.AddAttribute(1, "class", "chat-message");

    builder.OpenElement(2, "span");
    builder.AddAttribute(3, "class", "time");
    builder.AddContent(4, ToLocal(Message.Time, tp));
    builder.CloseElement();

    builder.OpenElement(5, "span");
    builder.AddAttribute(6, "class", "author");
    builder.AddContent(7, Message?.Author ?? "Unknown");
    builder.CloseElement();

    builder.OpenElement(8, "span");
    builder.AddAttribute(9, "class", "text");
    builder.AddContent(10, Message?.Text ?? "No message content");
    builder.CloseElement();

    builder.OpenElement(11, "button");
    builder.AddAttribute(12, "onclick", EventCallback.Factory.Create(this, LikeMessage));
    builder.AddContent(13, "Like");
    builder.CloseElement();

    builder.CloseElement();
  
    Task LikeMessage()
    {
        var like = Message.Likes + LikeMultiplier;
        return OnLike.InvokeAsync(like);
    }

    static DateTimeOffset ToLocal(DateTimeOffset time, TimeProvider timeProvider)
    {
        return TimeZoneInfo.ConvertTime(time, timeProvider.LocalTimeZone);
    }
  }
}

And be used as usual, i.e.:

@inject MessageRepo repo
<ChatMessage Message=@message OnLike=@OnLike />
@code {
  private ChatMessage? message;

  protected override async Task OnInitializedAsync()
  {
      message = await repo.LoadMessage();
  }
  
  private async Task OnLike(int likes)
  {
    message.Likes += likes;
    await repo.SaveAsync(message);
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-blazorIncludes: Blazor, Razor Componentsdesign-proposalThis issue represents a design proposal for a different issue, linked in the descriptionenhancementThis issue represents an ask for new feature or an enhancement to an existing onefeature-blazor-component-modelAny feature that affects the component model for Blazor (Parameters, Rendering, Lifecycle, etc)

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions