Blazor WebAssemblyで画面の処理やレイアウトを共通化しようとしたとき、
Fluxorという状態管理ライブラリを使用してとても便利だったので、今後の自分のためにメモ
概要
親子のコンポーネント間でのデータのやり取りはパラメータ、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画面で名前を入力すると、他画面にも表示されるような実装を行いたいと思います
以下のような構想で実装していきたいと思います。
Fluxorの導入
Blazor WebAssemblyのプロジェクトを作成し、NuGetパッケージからFluxor.Blazor.Web
とFluxor.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を確認することができます。
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.razor
とNameComponent.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を使って一元管理するだけで、ワークフローの実装を高速化でき、さらに拡張しやすくなりました。
まだまだ初心者のため、改善策や間違っている箇所等ありましたら、ご指摘お願いします。