LWCでReduxを使って状態管理をする

Lightning Web Componentの不満の一つは状態管理の仕組みがないことです。

コンポーネント間連携は[イベント](https://developer.salesforce.com/docs/component-library/documentation/en/lwc/lwc.events)もしくは[Lightning Message Service（LMS）](https://developer.salesforce.com/docs/component-library/documentation/en/lwc/lwc.use_message_channel)を使えということになっています[^1]が、大きめのアプリをコンポーネントベースで作る場合、これらの方法でコンポーネント間連携を行うとすぐに破綻することは目に見えています。

[^1]: 昔はlwc-recipesでpubsub.jsというのがあって、まじかよと思ったものですが、今はLMS使えと書き換わっていました。[lwc-recipes/force-app/main/default/lwc/pubsub at 0fbfebe17868b3116ed0d2eee962fc29a5a4dc36 · trailheadapps/lwc-recipes](https://github.com/trailheadapps/lwc-recipes/tree/0fbfebe17868b3116ed0d2eee962fc29a5a4dc36/force-app/main/default/lwc/pubsub)

アプリケーションビルダーに配置する単位のコンポーネント間連携はLMSでいいと思うんですが、コンポーネントを組み合わせて複雑なアプリを作るというケースを考慮してもらいたいものです。

React/Vue/AngularなどのコンポーネントベースのJavaScriptフレームワークでのアプリ開発では状態管理フレームワークが一般的に使われています。Reactで言えば、Reduxが一番人気のある状態管理ライブラリです。[^2]

[^2]: 今はHooks APIの方が多いのでしょうか？

実際、私がLWC OSSで作成した[LWC SOQL Builder](https://qiita.com/atskimura/items/b06a5ad4dc9e1905c29f)でもReduxを使用しています。その際はLWC OSSでReduxを使っているサンプルコードがありましたので、それを参考に実装しましたが、Salesforce Platform版LWCに移植するのはなかなか大変そうでした。ちょっと探してみたところ、[chandrakiran-dev/lwc-redux](https://github.com/chandrakiran-dev/lwc-redux)というよさげな実装を見つけたので、今回はこちらをご紹介します。

https://github.com/chandrakiran-dev/lwc-redux

# Reduxとは？

[Redux](https://redux.js.org/)については公式サイトも詳しいですし、たくさんの人が解説記事を書いていますので、そちらをご覧ください。

UIレイヤーには依存しないように作られているので、[様々なフロントエンドフレームワークと連携するライブラリ](https://github.com/markerikson/redux-ecosystem-links/blob/master/library-integration.md)があります。lwc-reduxはそのLWC版です。

# lwc-reduxの使い方

https://www.lwc-redux.com/

基本的な使い方はHooks APIを使わない[react-redux](https://react-redux.js.org/)とだいたい同じです。公式のクイックスタートとチュートリアルを読むとだいたいわかるかと思いますが、簡単に説明します。

解説はreact-reduxからコピーしてきた感じのところも多くちょっと疑問なところもあります。ソースコードが短めなのでわからないところはソースコードを読むと良いかと思います。

## Provider

アプリのルートコンポーネントを`c-provider`コンポーネントの子にすることで、アプリでReduxが利用可能になります。

```html:counterContainer.html
<template>
    <c-provider oninit={initialize} store={store}>
        <c-counter></c-counter>
    </c-provider>
</template>
```

`c-lwcRedux`コンポーネントが`createStore`,`combineReducers`を提供します。

```js:counterContainer.js
import { LightningElement, api } from 'lwc';
import {createStore, combineReducers} from 'c/lwcRedux';
import reducers from 'c/counterReducers';

export default class CounterContainer extends LightningElement {
    @api store;
    initialize(){
        const combineReducersInstance = combineReducers(reducers);
        this.store = createStore(combineReducersInstance);
    }
}
```

## ReduxElement

LightningElementのMixinです。コンポーネントに`mapDispatchToProps`と`mapDispatchToProps`を使えるようにします。

以下のように使うことができ、`this.props.increment()`と呼ぶとincrementアクションをdispatchすることができます。

```js:counter.js
import { LightningElement } from 'lwc';
import { Redux } from 'c/lwcRedux';
import {increment, decrement, reset} from 'c/counterActions';

export default class Counter extends Redux(LightningElement) {
    mapStateToProps(state){
        return {counter: state.counter};
    }
    mapDispatchToProps(){
        return {increment, decrement, reset};
    }
}
```

lwc-reduxではコンポーネントから`store`に直接アクセスする手段は用意されておらず、基本的に`mapStateToProps`と`mapDispatchToProps`を使ってstoreにアクセスするようです。

# lwc-reduxの仕組み

どう動いているかを簡単に解説します。ProviderコンポーネントでReduxのstoreを管理し、Providerの子孫コンポーネントは以下のような動きをします。

## stateが更新されたとき

1. 各コンポーネントはコールバック関数を渡した`lwcredux__getstore`イベントを発火
2. Providerコンポーネントは`lwcredux__getstore`を受けて管理しているReduxのstoreを渡してコールバック関数を呼ぶ
3. 各コンポーネントはmapStateToPropsを呼んで`this.props`を更新
4. `this.props`は`@track`になっているので、各コンポーネントは変更を検知できる

## 各コンポーネントがactionをdispatchするとき

こちらはシンプルで`mapDispatchToProps`でアクションを渡してあげると`bindActionCreators`でpropsにそれをdispatchする関数をセットしてくれます。その関数を実行すると、stateが更新されるので、上の手順が動きます。

TODO: 簡単に説明

# ToDoオブジェクト連動するToDoアプリを作る

公式サンプルがいくつかあるのですが、redux-thunkを使った非同期処理についてはサンプルがなかったので、この記事では**チュートリアルのToDoアプリをSalesforceのToDoオブジェクトと連動するように改造**して、もう少し実践的なサンプルにしてみたいと思います。

できあがったものはGithubに上げています。
https://github.com/atskimura/lwc-redux-example

## 環境セットアップ

最初に環境セットアップです。

### sfdxプロジェクト作成

```
sfdx force:project:create -n lwc-redux-example
```

### スクラッチ組織作成

```
sfdx force:org:create -f config/project-scratch-def.json -a lwc-redux-example -d 30 -s
```

### lwc-reduxパッケージインストール

ロック解除済みパッケージで提供されているのでインストールします。

```
sfdx force:package:install --package 04t2v0000079AtRAAU -w 15
```

## 公式チュートリアルでToDoアプリを作る

公式チュートリアルを進めましょう。

http://lwc-redux.com/basic-tutorial

チュートリアルには以下の２つのコンポーネントがないので追加してください。

* force-app/main/default/lwc/todoAppConstant
* force-app/main/default/lwc/components/lwc/todoApp

```js:todoAppConstant.js
export const ADD_TODO = "ADD_TODO";
export const CHANGE_TODO_STATUS = "CHANGE_TODO_STATUS";
export const SET_FILTER = "SET_FILTER";

export const VISIBILITY_FILTER = {
    ALL: "All",
    COMPLETED: "Completed",
    INCOMPLETE: "Incomplete",
    INPROGRESS: "Inprogress"
}
export const STATUS = {
    COMPLETED: "Completed",
    INCOMPLETE: "Incomplete",
    INPROGRESS: "Inprogress"
}
```

```html:todoApp.html
<template>
  <lightning-card  title="To Do App">
      <div class="slds-p-around_medium">
          <c-add-todo></c-add-todo>
          <c-todo-filter></c-todo-filter>
          <c-todo-list></c-todo-list>
      </div>
  </lightning-card>
</template>
```

ここまででホームページ等に配置すると以下のように動作するアプリができたと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27267/b700002b-8bbd-cef4-684c-a2e11aced82a.png)

ここまでのコードは以下のコミットになります。
[atskimura/lwc-redux-example at 487be8bd75fdc70ba36ce9d90814a033b0851d54](https://github.com/atskimura/lwc-redux-example/tree/487be8bd75fdc70ba36ce9d90814a033b0851d54)

## ToDoオブジェクトと連動するように修正する

全部説明すると大変なので、タスク追加だけ説明しましょう。他は完成品のソースコードをご覧ください。

その修正のコミットはこちらです。
[Create Task record · atskimura/lwc-redux-example@d406e14](https://github.com/atskimura/lwc-redux-example/commit/d406e1478c60ae77b0df12a992296c641a01edb2)

### 1. Apexクラスを追加します

件名とステータスを受けてTaskレコードを作成するだけです。

```apex:TodoAppController.cls
public with sharing class TodoAppController {
    class TodoAppControllerException extends Exception {}

    @AuraEnabled
    public static Task createTask(String content, String status){
        try {
            Task task = new Task(
                Subject = content,
                Status = status
            );
            insert task;
            return task;
        } catch (Exception e) {
            throw new TodoAppControllerException(e.getMessage());
        }
    }
}
```

### 2. Action Typeを追加します

ADD_TODOが非同期処理になるので、`ADD_TODO_SUCCESS`と`ADD_TODO_ERROR`を追加します。

```js:force-app/main/default/lwc/store/lwc/todoAppConstant/todoAppConstant.js
export const ADD_TODO_SUCCESS = "ADD_TODO_SUCCESS";
export const ADD_TODO_ERROR = "ADD_TODO_ERROR";
```

### 3. Actionを修正します

lwc-reduxでは[redux-thunk](https://github.com/reduxjs/redux-thunk)が使えるので、Action Creatorは関数を返すことができます。
Apexメソッド`createTask`を呼び、結果に応じて`ADD_TODO_SUCCESS`または`ADD_TODO_ERROR`をdispatchします。
必要なimport文は適宜追加してください。

```js:force-app/main/default/lwc/store/lwc/todoAppActions/todo.js
export const addTodo = (content) => {
  return async (dispatch) => {
    try {
      const data = await createTask({content, status:STATUS.NOT_STARTED});
      dispatch({
        type: ADD_TODO_SUCCESS,
        payload: {data}
      });
    } catch(error) {
      dispatch({
        type: ADD_TODO_ERROR,
        payload: {
          error: {
            ...error,
            message: error.message || (error.body && error.body.message),
          }
        }
      });
    }
  };
};
```

### 4. Reducerを修正します

ADD_TODOアクションが発生したときはただ追加されたフラグを立てるだけで、実際にstateのToDoリストに追加するのはADD_TODO_SUCCESSアクションを受けたときに行います。

```js:force-app/main/default/lwc/store/lwc/todoAppReducer/todo.js
       case ADD_TODO: {
        return {
            ...state,
            isAdding: true
        };
       }
       case ADD_TODO_SUCCESS: {
        const { data } = action.payload;
        return {
            ...state,
            isAdding: false,
            allIds: [...state.allIds, data.Id],
            byIds: {
                ...state.byIds,
                [data.Id]: {
                    content: data.Subject,
                    status: data.Status
                }
            }
        };
       }
       case ADD_TODO_ERROR: {
        const { error } = action.payload;
        return {
            ...state,
            isAdding: false,
            addResult: { error }
        };
       }
```

ローディング表示やエラー表示をする場合は、`isAdding`や`addResult`を`c:todoList`コンポーネントで受けて処理しますが、このサンプルではそこは手を抜いて実装していません。ですので、表示用コンポーネント側の修正はありません。

### ToDoレコード一覧の取得/ステータスの変更の修正

同じようにToDoレコード一覧の取得、ステータスの変更も修正します。これらは説明しませんが、以下のコミットがそれらの修正に対応しています。

* [Fetch Task records in Salesforce org · atskimura/lwc-redux-example@f143381](https://github.com/atskimura/lwc-redux-example/commit/f143381467e411a0f833f1030a884cdda21588f9)
* [Update Task status · atskimura/lwc-redux-example@95d72f4](https://github.com/atskimura/lwc-redux-example/commit/95d72f4b6364e33bf24e9b75103b002483de0858)

## 完成

完成すると以下のようにToDoレコードがコンポーネントに表示されるようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27267/ee9b13fc-6db2-99e4-94c3-0b497cff5587.png)

## ソースコード

いきなり一番下を見る人もいると思うので、こちらにもリンクを貼っておきます。
https://github.com/atskimura/lwc-redux-example

# 終わりに

まだ私も本番で使ったことはないですが、コードもシンプルですし、割とよいのではないでしょうか？
イベントのdetailプロパティで関数を渡せるなんてことは知らなかったので、この実装は将来も大丈夫なのか不安な感じはなくもないですが。
Salesforceが標準で状態管理の仕組みを用意してくれるとうれしいですね。