BlazorでFluxorを使った状態管理

(An [English version](https://www.linkedin.com/pulse/state-management-blazor-using-fluxor-hideki-saito/) of this article is also available.)

この記事では[Blazor](https://blazor.net)で状態管理（State Management）を行う手法について紹介していきます。
今回使用したのは[Fluxor](https://github.com/mrpmorris/fluxor)という.NET向けのFlux/Redux実装です。

# 状態管理とは
状態管理、State Managementとは多くのコンポーネントに散らばりがちなアプリケーション内の状態を集約する形でアプリケーションの状態変化（ユーザーの入力等）に柔軟に対応できるようなシステムです。
# 使用したプロジェクト
今回使用したプロジェクトは拙作の[仙狐ビュワー](https://senko.hidekisaito.com/)です。完全なソースは公開していませんが、仕組み自体はシンプルなプロジェクトですので、コードサンプルを交え、その実装の仕組みについて紹介していきます。

## プロジェクトの内容
仙狐ビュワーは高解像度の画像を公開するためのシステムで、要はスライドショーアプリケーションです。URLからのパラメーターの受け取りや自動ページ切り替えなどの機能も実装していますが、今回の状態管理とはあまり関係していませんので、説明は省きます。

仙狐ビュワーでは画像情報を示したJSONファイルを読み取り、そのJSONファイルを元にインデックスを作成、それを目次として使い、画像を読み込む、という仕組みになっています。

## 以前の実装
以前使用していたシンプルな実装においてはただ単に、インデックスを保持し、ボタンの押下に対してそのインデックスを書き換える方法でした。この手のプログラムにおいては教科書的なかなりシンプルな実装です。

```csharp
<div>
<img src="@imageName"/>
</div>

@code 
{
  public string imageName;
  List<SenkoImage> images
  private int index = 0;

  public class SenkoImage
  {
    public string FileLocation;
    public string FileId;
  }

  public void Next()
  {
    index++;
    
    // ... 最後のページに達した場合の処理（0に戻す）

    imageName = images[index].FileLocation;
  }

  public void Prev()
  {
    index--;
    
    // ... 最初のページに達した場合の処理（最大ページに戻す）

    imageName = images[index].FileLocation;
  }

  public void Set(int index)
  {
    // ... indexが正常値かどうかを確認

    imageName = images[index].FileLocation;
  }

  protected override async Task OnAfterRenderAsync(bool firstRender)
  {
    // リストの初期化など
  }
}
```

実際はこの程度の実装であればこのような形で行うのも全く問題ないかと思います。ただ、ここでは状態管理の実装を試してみる、というモチベーションがありますので、敢えて、これを状態管理の手法で実装してみます。

# Fluxorを導入
早速Fluxorを導入していきます。
尚、Fluxorは状態管理そのものを実装しているシステムであり、必ずしもBlazorと合わせて使う必要がありません。実際にコマンドラインアプリでこれを使う方法も[紹介](https://github.com/mrpmorris/Fluxor/blob/master/Tutorials/01-BasicConcepts/01A-StateActionsReducersTutorial/README.md)されています。
今回はBlazorアプリで使用しますので、<code>Fluxor.Blazor.Web</code>パッケージをプロジェクトに追加して下さい。執筆時点でのバージョンは3.1.1です。

## 有効化の方法

有効化に必要なコードは[ドキュメント](https://github.com/mrpmorris/Fluxor/blob/master/Tutorials/02-Blazor/02A-StateActionsReducersTutorial/README.md)でも紹介されていますが、概ね以下のとおりです。（他のライブラリで使用される方法とあまり違いはありません。）

<code>www/index.html</code>（Blazor Serverの場合<code>Pages/_Host.cshtml</code>）に以下を追加。

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

<code>Program.Main</code>メソッドに以下を追加。

```csharp
var currentAssembly = typeof(Program).Assembly;
builder.Services.AddFluxor(options => options.ScanAssemblies(currentAssembly));
```

Blazor Serverの場合は次を<code>Startup.ConfigureServices</code>メソッドに追加します。

```csharp
var currentAssembly = typeof(Startup).Assembly;
services.AddFluxor(options => options.ScanAssemblies(currentAssembly));
```

<code>App.razor</code>に以下を追加。

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

# 実装
ここから実装していきます。

## 状態の定義
先ずは状態の定義を行います。今回の例では単一のシンプルなルールですので<code>Store/SlideUseCase/SlideState.cs</code>を作成し、以下のように定義しました。

```csharp
namespace SenkoViewer.Store.SlideUseCase
{
    public class SlideState
    {
        public SlideState(int slideCount)
        {
            SlideCount = slideCount;
        }

        public int SlideCount { get; }
    }
}
```

これが状態を格納するためのクラスになります。

次に、Featureを定義します。こちらは<code>Store/SlideUseCase/Feature.cs</code>として作成しました。

```csharp
using Fluxor;

namespace SenkoViewer.Store.SlideUseCase
{
    public class Feature : Feature<SlideState>
    {
        public override string GetName()
        {
            return "Slide";
        }

        protected override SlideState GetInitialState()
        {
            return new SlideState(0);
        }
    }
}
```

## 状態変移の実装
ここで各機能を実装していきますが、今回の例では大きく三種類の機能を持たせます。以下のとおりです。

- 次のページに移行する<code>IncrementSlide</code>
- 前のページに移行する<code>DecrementSlide</code>
- 任意のページに移行する<code>SetSlide</code>

まずは<code>IncrementSlide</code>を実装します。<code>DecrementSlide</code>に関してはこれと同様ですが、インデックスを減らす処理になります。
<code>Store/SlideUseCase/IncrementSlideAction.cs</code>に以下のようにしました。こちらは空で問題ありません。

```csharp
namespace SenkoViewer.Store.SlideUseCase
{
    public class IncrementSlideAction
    {
    }
}
```

次に<code>Store/SlideUseCase/Reducers.cs</code>に以下のように記述します。

```csharp
using Fluxor;

namespace SenkoViewer.Store.SlideUseCase
{
    public class Reducers
    {
        [ReducerMethod]
        public static SlideState ReduceIncrementSlideAction(SlideState state, IncrementSlideAction action)
        {
            return new SlideState(state.SlideCount + 1);
        }
    }
}
```

## 表示ロジックからの使用
さて、これを使用するためにはロジックから使用することにします。仙狐ビュワーにおいては、まず、画像表示部を専用のコンポーネントに移動させ、以下のような形で画像を呼び出せる形にしました。

```
<SenkoDisplay Index="0"/>
```

ですので、razorファイルの中では以下のように定義されています。これにより、画像のインデックスは状態管理のものが読み出されます。

```
<SenkoDisplay Index="@SlideState.Value.SlideCount"/>
```

まず、使用するrazorファイルに以下を追加します。

```
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
```

その上でDispatcherをインジェクトします。

```csharp
[Inject] public IDispatcher Dispatcher { get; set; }
```

また、各状態ごとにインターフェースもインジェクトします。

```csharp
[Inject] private IState<SlideState> SlideState { get; set; }
```

次に進めるためのメソッドは以下のようにします。

```csharp
public async Task Next()
{
  var action = new IncrementSlideAction();
  Dispatcher.Dispatch(action);
            
  await InvokeAsync(StateHasChanged);
}
```

これでこのメソッドが呼び出されると次の画像が表示される形になります。

## 任意の画像を設定できるようにする
仙狐ビュワーの機能として、パラメーターから提示した識別子（画像のハッシュ値）をもとに画像を呼び出したり、また、ランダムに画像を表示する機能も含んでいます。そのため、現在の形ではそれが行なえません。

そのため、<code>SetSlide</code>として、これを行うためのものを実装していきます。

<code>Store/SlideUseCase/SetSlideAction.cs</code>はIncrementやDecrementのものと少し違うものになります。（これはIncrementやDecrementでは空の実装だったものです。）

```csharp
namespace SenkoViewer.Store.SlideUseCase
{
    public class SetSlideAction
    {
        public SetSlideAction(int index)
        {
            Value = index;
        }

        public int Value { get; set; }
    }
}
```

これにより、アクションに<code>Value</code>を渡せるようにします。

Reducerに関してはこれから取り出した値を設定する形になります。

```csharp
[ReducerMethod]
public static SlideState ReduceSetSlideAction(SlideState state, SetSlideAction action)
{
    return new SlideState(action.Value);
}
```
これにより、以下のような形で任意の画像を表示できるようになります。

```csharp
var action = new SetSlideAction(5);
Dispatcher.Dispatch(action);
```

## ページのループを設定する
現在の実装のままでは最後のページに次、最初のページに前に進むような形になった場合に、例外が発生してしまいます。

そのため、<code>Store/SlideUseCase/SlideState.cs</code>を次のように変更してみます。

```csharp
using SenkoViewer.AssetAccessor;

namespace SenkoViewer.Store.SlideUseCase
{
    public class SlideState
    {
        public SlideState(int slideCount)
        {
            if (slideCount > Max - 1)
                SlideCount = 0;
            else if (slideCount < 0)
                SlideCount = Max - 1;
            else
                SlideCount = slideCount;
        }

        public int SlideCount { get; }
    }
}
```

# 最後に
仙狐ビュワーの場合には機能がさほど複雑ではないところもあり、これを行うことで却ってコード量が反対に増えてしまう形になります。より複雑な状態を管理しないといけない場合において、状態管理はその威力を発揮するかと思います。

Fluxor自体はBlazorアプリ以外でも使用できるようになっている点など、他にも応用することができ、ソリューションを問わず、状態管理機構が必要な場合において有用かと思います。

Fluxorにはこの他、Effectorなど他の使用ケースなども使用することが出来ますが、これはまたの機会に紹介していきたいと思います。