【Blazor ベストプラクティス】ShouldRenderを利用して不必要なレンダリングを避ける

## 概要

ページを構成するコンポーネントは親子関係になっていることが大半で、親がレンダリングされたら、子もレンダリングされ、さらに孫も、さらにひ孫も・・・・というように再レンダリングが末端まで続きます。

**しかし！**

Blazorにはデフォルトで、再レンダリングを実行するかどうかを判断する機能がついているんです！

本記事ではそれを有効利用する方法について紹介したいと思います。

## 再レンダリングの流れ

親コンポーネントでイベントが発生したときのレンダリングの流れは以下になります。

1. イベント実行を完了した親コンポーネントが再レンダリングされる
1. 再レンダリングの際、子コンポーネントに渡すパラメータには新たなコピーが渡される
1. パラメータを受け取った子コンポーネントはパラメータ値に変化があるかをチェックし、**変化があれば再レンダリングを行い、なければ再レンダリングしない**

めちゃくちゃ気が利いていますね！もうこれで紹介が終わりそうな感じもしますが、知っておかなければいけない癖があります。

それは、**パラメータ値がプリミティブ型の場合のみ、変化有無の判断が行われる**という点です。**自作クラスなどをパラメータとして渡す場合は問答無用で再レンダリングされます。**

※これは私の理解ですが、上記の手順２において参照型は新たな参照値を生成し、レンダリングの度に異なる値となるため問答無用でレンダリングが走ると認識しています。

## `ShouldRender`の活用

自作クラスなどを渡す場合でも、`ShouldRender`を有効利用することで不要なレンダリングを防ぐことができます。

### サンプルで確認

確認に必要なコンポーネント群を準備します。各コンポーネントの`OnAfterRender`でレンダリング有無のログを確認します。

Blazorプロジェクトは、`WebAssembly`で作成しています。

※もちろん`Server`でも確認可能ですが、ログ出力の部分を`System.Diagnostics.Debug.Write`に変更する必要があります。

- パラメータ無しコンポーネント

```c#:NoParamComponent.razor
<h3>NoParamComponent</h3>

@code {
    protected override void OnAfterRender(bool firstRender)
    {
        if (!firstRender)
            Console.WriteLine("NoParamComponent");
    }
}
```

- プリミティブ型パラメータ有りコンポーネント

```c#:PrimitiveParamComponent.razor
<h3>PrimitiveParamComponent</h3>

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

    protected override void OnAfterRender(bool firstRender)
    {
        if (!firstRender)
            Console.WriteLine("Primitive Param Component");
    }
}
```
- 自作クラス型パラメータ有りコンポーネント

```c#:Models/UserInfo.cs
namespace YourProject.Models;

public class UserInfo
{
    public string Name { get; set; } = "";
    public string Email { get; set; } = "";
}
```

```c#:CustomClassComponent.razor
@using YourProject.Models
<h3>CustomClassParamComponent</h3>

@code {
    [Parameter]
    public UserInfo UserInfo { get; set; } = default!;

    protected override void OnAfterRender(bool firstRender)
    {
        if (!firstRender)
            Console.WriteLine("Custom Class Param Component");
    }
}
```

- 親コンポーネント

再レンダリングを発火するボタンを持ちます。

```c#:ParentComponent.razor
@page "/effective-rendering/parent-component"
@using BlazorAppTips.Client.Models
@rendermode InteractiveWebAssembly

<button @onclick="() => isToggle = !isToggle" style="border: 1px solid black">Click</button>
@isToggle
<NoParamComponent />
<PrimitiveParamComponent Title=@title />
<CustomClassParamComponent UserInfo=@userInfo />

@code {
    private bool isToggle = false;
    private string title = "title";
    private UserInfo userInfo = new UserInfo();

    protected override void OnAfterRender(bool firstRender)
    {
        if (!firstRender)
            Console.WriteLine("Parent Component");
    }
}
```

### `ShouldRender`無しの場合

`http://localhost:[Your port]/effective-rendering/parent-component`にアクセスし、Clickを押下します。

Browserのコンソールログから`ParentComponent`と`CustomClassComponent`がレンダリングされていることが確認できます。

`userInfo`の**値は何一つ変わっていない**のに、再レンダリングが実行されていることが分かりますね。

ちなみに、`NoParamComponent`はパラメータ値が存在しないので、再レンダリングが行われず、`PrimitiveParamComponent`はパラメータ値がプリミティブ型かつ値も変わっていないので再レンダリングが発火されていません。

### `ShouldRender`有りの場合

`ShouldRender`を利用するように、`CustomClassComponent.razor`を以下のように変更してください。

**前回値と再設定されるパラメータ値を比較し、異なっていれば`ShouldRender`に`true`が格納**され再レンダリングを実行します。

この状態で、Clickを押下すると、`ParentComponent`のみレンダリングされていることが分かりますね！

```c#:CustomClassComponent.razor
@using YourProject.Models
<h3>CustomClassParamComponent</h3>

@code {
    private string prevUserInfoName = "";
    private string prevUserInfoEmail = "";
    private bool shouldRender = false;

    [Parameter]
    public UserInfo UserInfo { get; set; } = default!;

    protected override void OnAfterRender(bool firstRender)
    {
        if (!firstRender)
            Console.WriteLine("Custom Class Param Component");
    }

    protected override void OnParametersSet()
    {
        // Judge this component shyould render or not
        shouldRender = prevUserInfoName != UserInfo.Name || prevUserInfoEmail != UserInfo.Email;

        prevUserInfoName = UserInfo.Name;
        prevUserInfoEmail = UserInfo.Email;
    }

    protected override bool ShouldRender() => shouldRender;
}
```

## 最後に

Callbackを渡したい場合等も同様に`ShouldRender`を用いて制御可能です。アプリケーション規模が大きくなればなるほど、こういった効率化が役に立ってきます。

ただ機能を実装するのではなく、**フレームワークを駆使して効率的な開発**をできるように心がけていきたいものです。


## リポジトリ

[BlazorAppTips](https://github.com/HIppei/BlazorAppTips)


## 参考

[Avoid unnecessary rendering of component subtrees](https://learn.microsoft.com/en-us/aspnet/core/blazor/performance?view=aspnetcore-8.0#avoid-unnecessary-rendering-of-component-subtrees)