In Blazor, is there a better way to add elements from a list?

Viewed 768

I'm doing some tests with DotNet 5rc1 and Blazor, and have noticed something that makes me think I'm not doing this right.

I have a demo page (code below) that gives the user a set of buttons and they can click on a button to do a die roll. In the client side C# I then roll the die and prepend the roll string to a list. In the template, I then do a foreach and render the user's past rolls.

The issue is that I've been watching the Websocket messages and each time I add a new element, the message being passed gets larger and larger. This is because the entire list is being re-rendered with each click.

Is there a better way to do this, so that Blazor only inserts the new item in the DOM and sends down a trivial message, rather than re-rendering the entire section of the page? For this example, it doesn't matter much, but if I were building a bigger app with a lot of interactions, I could see this becoming a bottleneck.

Here's the code for my page - this is just a new page in a brand new Blazor project. (My code has more than a 1d20 button, but I removed it from here for brevity.)

@page "/dieroller"

<h1>Die Roller Utility</h1>

...

<button class="btn btn-primary" @onclick="@(e => RollDie(20))">
    <i class="fas fa-dice-d20"></i>
</button>

<h2 class='mt-5'>Your Roll History</h2>

@foreach (string roll in rolls) {
    <p>@roll</p>
}

@code {
    @using System.Threading;
    private List<string> rolls = new List<string>();

    private void RollDie(int upper)
    {
        Random rand = new Random();
        rolls.Insert(0, string.Format("1d{0} = {1}", upper, rand.Next(1, upper)));
    }
}
3 Answers

The direct answer to your problem is to use @key. That is always a good idea when a loop is involved.

The problem is that you need unique keys and your strings don't fit that requirement.

A blunt solution is to introduce a class (not a struct), just to get unique keys. I adapted your code to the following:

@foreach (Roll roll in rolls)
{
    <p @key="roll">@($"d={roll.Upper} {roll.Value}")</p>
}

@code {

    class Roll
    {
        public int Upper { get; set; }
        public int Value { get; set; }
    }

    private List<Roll> rolls = new List<Roll>();

    private void RollDie(int upper)
    {
        Random rand = new Random();
        rolls.Insert(0, new Roll { Upper = upper, Value = rand.Next(1, upper + 1) });
    }
}

Using this you do get stable, non-growing WS packets.

Based on @BrianParker's comment above - Yes, you need a key so that Blazor knows which items need to be updated. That Key also needs to be unique, or you'll generate errors with key collision.

Because these are strings, they don't make good keys - therefore I ended up retooling this with a DieRoll class that I could put in a list. Here's the new code behind:

using System;
using System.Collections.Generic;

namespace BlazorApp.Pages
{
    public partial class DieRoller
    {
        private List<DieRoll> rolls = new List<DieRoll>();

        private void RollDie(int upper)
        {
            rolls.Insert(0, new DieRoll(upper));
        }
    }

    public class DieRoll
    {
        public int Roll;
        public int DieType;

        public DieRoll(int DieType) {
            this.DieType = DieType;
            RollDie();
        }

        public int RollDie() {
            Random rand = new Random();
            this.Roll = rand.Next(1, DieType + 1);
            return Roll;
        }

        public override string ToString()
        {
            return string.Format("1d{0} = {1}", this.DieType, this.Roll);
        }

    }
        
}

And here's the new template code at the p tag:

@foreach (DieRoll die in rolls) {
    <p @key="die">@die</p>
}

Obviously this is more complex, but this is what works. The messages from the server are much smaller, and never grow in size.

Also, this might NOT have mattered if I wasn't prepending the items to the List. Appending to the list might have let Blazor understand where the elements were being created easier. But I didn't bother to test that theory.

You can see another example here: https://blazor-university.com/components/render-trees/optimising-using-key/

With List.Add the messages size remains the same, so why is List.Insert any different? Presumably it's due to the inherent performance penalty that List.Insert(index: 0) incurs since the underlying array needs to be reshuffled, see here: https://stackoverflow.com/a/18587349/4000335 To verify this even using List.Insert(List.Count) shows the message size remains the same (as is the case with Add).

If the requirement is to have the last roll on top then an optimal approach would probably involve JavaScript, which defeats the purpose of using blazor server in the first place, however, blazor webassembly doesn't need to communicate with a server to accomplish this since it's all done on the client.

Related