16
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Salesforce PlatformAdvent Calendar 2020

Day 21

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

Last updated at Posted at 2020-12-21

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

コンポーネント間連携はイベントもしくはLightning Message Service(LMS)を使えということになっています1が、大きめのアプリをコンポーネントベースで作る場合、これらの方法でコンポーネント間連携を行うとすぐに破綻することは目に見えています。

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

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

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

Reduxとは?

Reduxについては公式サイトも詳しいですし、たくさんの人が解説記事を書いていますので、そちらをご覧ください。

UIレイヤーには依存しないように作られているので、様々なフロントエンドフレームワークと連携するライブラリがあります。lwc-reduxはそのLWC版です。

lwc-reduxの使い方

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

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

Provider

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

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

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

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です。コンポーネントにmapDispatchToPropsmapDispatchToPropsを使えるようにします。

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

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に直接アクセスする手段は用意されておらず、基本的にmapStateToPropsmapDispatchToPropsを使って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アプリを作る

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

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

  • force-app/main/default/lwc/todoAppConstant
  • force-app/main/default/lwc/components/lwc/todoApp
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"
}
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

ここまでのコードは以下のコミットになります。
atskimura/lwc-redux-example at 487be8bd75fdc70ba36ce9d90814a033b0851d54

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

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

その修正のコミットはこちらです。
Create Task record · atskimura/lwc-redux-example@d406e14

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

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

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_SUCCESSADD_TODO_ERRORを追加します。

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が使えるので、Action Creatorは関数を返すことができます。
ApexメソッドcreateTaskを呼び、結果に応じてADD_TODO_SUCCESSまたはADD_TODO_ERRORをdispatchします。
必要なimport文は適宜追加してください。

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アクションを受けたときに行います。

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 }
        };
       }

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

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

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

完成

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

image.png

ソースコード

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

終わりに

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

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

  2. 今はHooks APIの方が多いのでしょうか?

16
8
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
16
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?