0
0

Blazor WebAssemblyで共通コンポーネントで画面を構成するときにFluxorが便利だった

Posted at

Blazor WebAssemblyで画面の処理やレイアウトを共通化しようとしたとき、
Fluxorという状態管理ライブラリを使用してとても便利だったので、今後の自分のためにメモ:writing_hand::writing_hand:

概要

親子のコンポーネント間でのデータのやり取りはパラメータ、EventCallback、CascadingParameterなどを使用すると思います。
共通化を進めていくと、どのイベントを元にデータが書き換わるのか複雑化し、少しの変更作業に時間がかかってしまったり、バグが生まれやすかったりしてしまいます。
ですが、Fluxorを使用することでどのイベントを元にデータが変更されるのか明確なので、変更作業がとても楽になりました。

使用した環境やVersionなど

  • .NET 8.0.204
  • Microsoft.AspNetCore.Components.WebAssembly 8.0.4
  • Fluxor.Blazor.Web 5.9.1
  • Fluxor.Blazor.Web.ReduxDevTools 5.9.1

やりたいこと

  • 画面で入力したデータを他画面に共通コンポーネントを使用して表示したい
  • パラメータなどを使用せずにデータを受け渡したい

イメージ

Home画面で名前を入力すると、他画面にも表示されるような実装を行いたいと思います
スクリーンショット 2024-05-06 120710.png

スクリーンショット 2024-05-06 120909.png

以下のような構想で実装していきたいと思います。

スクリーンショット 2024-05-06 120512.png

Fluxorの導入

Blazor WebAssemblyのプロジェクトを作成し、NuGetパッケージからFluxor.Blazor.WebFluxor.Blazor.Web.ReduxDevToolsをインストールします。

補足

Fluxor.Blazor.Web.ReduxDevToolsを追加すると、Chromeの開発ツールにReduxというタブが追加され、そこでStateの状態をDebugで確認することができます。
不要の場合は、追加しなくても大丈夫ですが、とても便利なので今回は追加しておきます。

初期設定

1. index.htmlにjavascriptファイルを追加

wwwroot配下にあるindex.cshtmlに以下を追加します

<script src="_content/Fluxor.Blazor.Web/scripts/index.js"></script>

2.App.razorに自動初期化処理を追加

初期化を行わずに制御したい場合は、以下のコードを追加せず手動で初期化のコーディングをすることも可能ですが、今回は割愛します。

<Fluxor.Blazor.Web.StoreInitializer></Fluxor.Blazor.Web.StoreInitializer>

3.Program.csにFluxorを追加

UseReduxDevTools();については、ユーザにStateを見せないようにするために、# if Debugなどを追加していただいてもいいかもしれません。

// Add Fluxor
builder.Services.AddFluxor(config =>
{
    config
        .ScanAssemblies(typeof(Program).Assembly)
        .UseReduxDevTools();  // Debug時にStateを確認するため
});

実装

No.1~No.3の実装は本質的なところではないので、飛ばしていただいても構いません。

1.Home.razor

Home.razor.csをPagesフォルダ配下に追加し、以下のように実装

public partial class Home
{
    [Inject]
    protected IDispatcher Dispatcher { get; set; } = default!;

    /// <summary>
    /// 名前
    /// </summary>
    private string? Name;

    /// <summary>
    /// 名前が変更されたときの処理
    /// </summary>
    private void OnChangedName(ChangeEventArgs e)
    {
    }
}

Home.razorに以下を追加

<div class="mb-3 row">
	<div class="col-6">
		<label for="Input" class="form-label">あなたの名前は?</label>
		<input @onchange="OnChangedName" class="form-control"/>
	</div>
</div>

2.NameComponent.razor

NameComponent.razorをPagesフォルダ配下に追加し、以下を追加します。

<div class="alert alert-primary" role="alert"><strong>あなたの名前は</strong> - ●●</div>

3.Counter.razorとWeather.razor

Counter.razorとWeather.razorの好きなところに、以下を追加します

<NameComponent></NameComponent>

4.Stateの実装

プロジェクトにStoreフォルダを追加し、Storeフォルダ配下にNameState.csを追加します。
NameState.csの内容は、以下の通りです。

イベントがトリガーされたときのみ値が更新されるようinitにしています。

public record NameState
{
    public string? Name { get; init; }
}

public class NameFeatureState : Feature<NameState>
{
    public override string GetName() => nameof(NameState);

    /// <summary>
    /// 初期化を行います
    /// </summary>
    /// <returns></returns>
    /// <exception cref="NotImplementedException"></exception>
    protected override NameState GetInitialState()
    {
        return new NameState
        {
            Name = string.Empty
        };
    }
}

上記を追加すると、ChromeでDebug時にStateを確認することができます。

スクリーンショット 2024-05-06 105842.png

5.Actionの実装

Storeフォルダ配下にChangeName.csを追加します。
ChangeName.csの内容は、以下の通りです。

Actionが実行されたときに渡したいプロパティなどを設定します。
渡すプロパティが何もない場合は、public record ChangeName{}のみです。

public record ChangeName
{
    public string? Name { get; set; }

    public ChangeName(string? name)
    {
        Name = name;
    }
}

6.Reducerの実装

Storeフォルダ配下にNameReducer.csを追加します。
NameReducer.csの内容は、以下の通りです。

先ほど追加したactionのプロパティから名前を取得して、状態を変更する内容になっています。

public static class NameReducer
{
    [ReducerMethod]
    public static NameState OnChangeName(NameState state, ChangeName action)
    {
        return state with
        {
            Name = action.Name
        };
    }
}

7.基本となるコンポーネントクラスの追加

BaseComponent.csを追加して、以下の内容を設定します。

public partial class BaseComponent : FluxorComponent
{
    /// <summary>
    /// NameState
    /// </summary>
    [Inject]
    protected IState<NameState> NameState { get; set; } = default!;

    /// <summary>
    /// 初期化を行います
    /// </summary>
    protected override void OnInitialized()
    {
        base.OnInitialized();
        // イベント設定
        NameState.StateChanged += OnChangedName;
    }

    /// <summary>
    /// Nameが変更されたときの処理
    /// </summary>
    /// <param name="obj"></param>
    /// <param name="e"></param>
    private void OnChangedName(object? obj, EventArgs e)
    {
        StateHasChanged();
    }
}

Home.razorNameComponent.razorに、以下を追加します

@inherits BaseComponent

8.Home.razorの修正

Home.razor.csは、以下のように修正します。

public partial class Home
{
    /// <summary>
    /// 状態変更イベントトリガー
    /// </summary>
    [Inject]
    protected IDispatcher Dispatcher { get; set; } = default!;

    /// <summary>
    /// 名前
    /// </summary>
    private string? Name;

    /// <summary>
    /// 名前が変更されたときの処理
    /// </summary>
    private void OnChangedName(ChangeEventArgs e)
    {
        Name = e.Value?.ToString();
        // NameStateを変更
        Dispatcher.Dispatch(new ChangeName(Name));
    }
}

Home.razorは、以下のように修正します。

@page "/"
@inherits BaseComponent
<PageTitle>Home</PageTitle>

<div class="mb-3 row">
	<div class="col-6">
		<label for="Input" class="form-label">あなたの名前は?</label>
		<input @onchange="OnChangedName" value="@NameState.Value.Name" class="form-control"/>
	</div>
</div>

9.NameComponent.razorの修正

NameComponent.razorは、以下のように修正します。

@inherits BaseComponent
<div class="alert alert-primary" role="alert"><strong>あなたの名前は</strong> - @NameState.Value.Name</div>

まとめ

Blazor WebAssemblyで共通化をしようとすると、様々なコンポーネントが相互に接続されているため、ひとつの処理を追加したいだけなのに、どこから始めたらよいか分かりにくいことが多くありました。

その相互接続をFluxorを使って一元管理するだけで、ワークフローの実装を高速化でき、さらに拡張しやすくなりました。

まだまだ初心者のため、改善策や間違っている箇所等ありましたら、ご指摘お願いします。

参考にしたリンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0