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

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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2647017/224b2d2a-1fa2-f59b-da75-7db7f4e369ac.png)

![スクリーンショット 2024-05-06 120909.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2647017/e2393e83-bd42-4c88-d503-2bf70b409543.png)



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

![スクリーンショット 2024-05-06 120512.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2647017/6d7c78f8-786d-9cac-f926-9b3dbdfdbceb.png)


# 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に以下を追加します

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

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

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

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

```csharp
// 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フォルダ配下に追加し、以下のように実装
```csharp
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に以下を追加
```html
<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フォルダ配下に追加し、以下を追加します。
```html
<div class="alert alert-primary" role="alert"><strong>あなたの名前は</strong> - ●●</div>
```

### 3.Counter.razorとWeather.razor
Counter.razorとWeather.razorの好きなところに、以下を追加します
```html
<NameComponent></NameComponent>
```

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

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

```csharp
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2647017/d3d892b0-4ff7-9bef-766e-6817168ea81e.png)

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

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

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

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

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

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

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

### 7.基本となるコンポーネントクラスの追加
`BaseComponent.cs`を追加して、以下の内容を設定します。

```csharp
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`に、以下を追加します

```html
@inherits BaseComponent
```

### 8.Home.razorの修正
`Home.razor.cs`は、以下のように修正します。
```csharp
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`は、以下のように修正します。
```html
@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`は、以下のように修正します。

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

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

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

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

# 参考にしたリンク
- https://github.com/mrpmorris/Fluxor