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が利用可能になります。
<template>
<c-provider oninit={initialize} store={store}>
<c-counter></c-counter>
</c-provider>
</template>
c-lwcRedux
コンポーネントがcreateStore
,combineReducers
を提供します。
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することができます。
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が更新されたとき
- 各コンポーネントはコールバック関数を渡した
lwcredux__getstore
イベントを発火 - Providerコンポーネントは
lwcredux__getstore
を受けて管理しているReduxのstoreを渡してコールバック関数を呼ぶ - 各コンポーネントはmapStateToPropsを呼んで
this.props
を更新 -
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
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"
}
<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>
ここまででホームページ等に配置すると以下のように動作するアプリができたと思います。
ここまでのコードは以下のコミットになります。
atskimura/lwc-redux-example at 487be8bd75fdc70ba36ce9d90814a033b0851d54
ToDoオブジェクトと連動するように修正する
全部説明すると大変なので、タスク追加だけ説明しましょう。他は完成品のソースコードをご覧ください。
その修正のコミットはこちらです。
Create Task record · atskimura/lwc-redux-example@d406e14
1. Apexクラスを追加します
件名とステータスを受けてTaskレコードを作成するだけです。
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
を追加します。
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文は適宜追加してください。
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アクションを受けたときに行います。
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
- Update Task status · atskimura/lwc-redux-example@95d72f4
完成
完成すると以下のようにToDoレコードがコンポーネントに表示されるようになります。
ソースコード
いきなり一番下を見る人もいると思うので、こちらにもリンクを貼っておきます。
https://github.com/atskimura/lwc-redux-example
終わりに
まだ私も本番で使ったことはないですが、コードもシンプルですし、割とよいのではないでしょうか?
イベントのdetailプロパティで関数を渡せるなんてことは知らなかったので、この実装は将来も大丈夫なのか不安な感じはなくもないですが。
Salesforceが標準で状態管理の仕組みを用意してくれるとうれしいですね。
-
昔はlwc-recipesでpubsub.jsというのがあって、まじかよと思ったものですが、今はLMS使えと書き換わっていました。lwc-recipes/force-app/main/default/lwc/pubsub at 0fbfebe17868b3116ed0d2eee962fc29a5a4dc36 · trailheadapps/lwc-recipes ↩
-
今はHooks APIの方が多いのでしょうか? ↩