【必読】なぜ今、Aura ではなく LWR を選ぶべきなのか?

【@lwc/state連載】第3回:組み込み状態マネージャーとSalesforceデータ連携の高度な統合パターン

この記事はバージョン Spring ’26 において執筆しています。
現在の動作と異なる場合がありますので、ご認識おきください。

第1回第2回の連載を通じて、@lwc/stateを利用してUIコンポーネントからビジネスロジックを分離し、fromContextを用いて複数コンポーネント間で安全に状態を共有する手法を学んできました。

しかし、実際のSalesforce開発において、状態管理の対象となるデータの大部分は「Salesforceのレコードデータ」や「オブジェクトのメタデータ」です。従来、LWCでこれらのデータを取得するには、コンポーネントのJavaScriptファイル内で@wireデコレータを使用してLightning Data Service(LDS)を呼び出すのが標準でした。

ここでアーキテクチャ上の大きな壁にぶつかります。@wireアダプターはコンポーネントのライフサイクルと強く結びついているため、UIを持たない純粋なJavaScriptモジュール(状態マネージャー)の内部では使用できないのです。

では、データ取得のロジックはこれまで通りUIコンポーネント側に残し、取得した結果だけをアクション経由で状態マネージャーに渡すべきでしょうか? Salesforceは、この問題をよりエレガントに解決するために「組み込み状態マネージャー(Built-in State Managers)」を提供しています。

DXforce Point

本機能はSpring ’26においてベータ版であり、Experience Cloudでの動作は未対応となっています。

DXforce Point for Developers

今回の記事で作成したコンポーネントの全ソースコードをGitHubで公開しています。ぜひ Clone して、あなたの組織で動かしてみてください。

LWRって何?どんなメリットがあるの?
そんな疑問を解決するにはまずは以下の記事をご覧ください。
Salesforce LWRとは? Experience Cloudの次世代ランタイムを徹底解説

組み込み状態マネージャー(Built-in State Managers)とは

Salesforceは、プラットフォームのデータとメタデータへアクセスするための標準機能として、以下のような組み込み状態マネージャー(ベータ版)を提供しています。

  • lightning/stateManagerRecord: 特定のレコードデータ(UI APIのgetRecord相当)を取得・管理する。
  • lightning/stateManagerLayout: オブジェクトのページレイアウト情報やメタデータを取得する。

これらは@wireの代わりとなるものであり、コンポーネントコンテキストに依存せず、独自のカスタム状態マネージャーの内部に「ネスト(入れ子)」して組み込むことができるよう設計されています。

これにより、データの取得から加工、そしてUIへの提供までを、単一のAPIモジュール内に完全にカプセル化することが可能になります。

実践:ネストされた状態マネージャーによるレコード管理

それでは、Lightning Experience環境で動作する「取引先(Account)ビューア」を例に、高度な状態マネージャーを構築してみましょう。

今回は、動的に渡されたレコードIDをもとに取引先データを取得し、必要に応じて別のレコードを読み込み直すことができる状態マネージャーを作成します。

高度な状態管理モジュール (accountStore)

ここでは、カスタムのdefineStateの中で、標準のstateManagerRecordをネストして使用します。

accountStore.js

import { defineState } from '@lwc/state';
// 組み込み状態マネージャーをインポート
import smRecord from 'lightning/stateManagerRecord';

export default defineState(({ atom, computed, setAtom }, initialRecordId) => {
    
    // 1. 組み込みマネージャーの設定情報を保持する「atom」を定義
    // この設定自体をリアクティブにすることで、動的な再フェッチが可能になります
    const recordConfig = atom({
        recordId: initialRecordId,
        fields: ['Account.Name', 'Account.Industry', 'Account.Phone']
    });

    // 2. ネストされた状態マネージャーの初期化
    // 設定atomを直接渡すことで、設定が変更されると自動的にデータを再取得します
    const recordManager = smRecord(recordConfig);

    // 3. UIコンポーネント向けに、使いやすい形にデータを整形(派生状態)
    // 組み込みマネージャーは status, data, error というプロパティを持ちます
    const isLoading = computed([recordManager], (mgr) => mgr.status === 'loading' || mgr.status === 'unconfigured');
    const errorMessage = computed([recordManager], (mgr) => mgr.error? mgr.error.body.message : null);
    
    const accountInfo = computed([recordManager], (mgr) => {
        if (mgr.status === 'loaded' && mgr.data) {
            return {
                name: mgr.data.fields.Name.value,
                industry: mgr.data.fields.Industry.value,
                phone: mgr.data.fields.Phone.value
            };
        }
        return null;
    });

    // 4. アクション:別のレコードIDを読み込む
    const loadNewAccount = (newRecordId) => {
        // 設定atomのrecordIdを更新するだけで、smRecordが自動的に新データをフェッチする
        setAtom(recordConfig, {
            recordId: newRecordId,
            fields: ['Account.Name', 'Account.Industry', 'Account.Phone']
        });
    };

    // 5. コンポーネントに公開するAPI
    return {
        isLoading,
        errorMessage,
        accountInfo,
        loadNewAccount
    };
});

UIコンポーネント (accountViewer)

状態マネージャーがデータ取得に関する複雑な非同期処理(ローディング状態の管理、エラーハンドリング、データの抽出など)をすべて引き受けてくれたため、UIコンポーネントのコードは極限までシンプルになります。

accountViewer.js

import { LightningElement, api } from 'lwc';
import accountStore from 'c/accountStore';

export default class AccountViewer extends LightningElement {
    // ページから渡されるレコードID
    @api recordId; 

    // コンポーネントの初期化時に状態マネージャーを生成
    // (※このコンポーネントをContextのプロバイダーとして配下の子コンポーネントに共有することも可能です)
    state;

    connectedCallback() {
        // レコードIDを初期値として渡してインスタンス化
        this.state = accountStore(this.recordId);
    }

    handleReloadAnother() {
        // サンプル:ボタンが押されたら別のデモ用取引先IDをロードする
        const anotherAccountId = '001gK00000TuugWQAR'; 
        this.state.value.loadNewAccount(anotherAccountId);
    }
}

accountViewer.html

<template>
    <lightning-card title="取引先ビューア(@lwc/state)" icon-name="standard:account">
        <div class="slds-p-around_medium">
            
            <template lwc:if={state.value.isLoading}>
                <lightning-spinner alternative-text="Loading" size="small"></lightning-spinner>
            </template>

            <template lwc:elseif={state.value.errorMessage}>
                <div class="slds-text-color_error">
                    データの取得に失敗しました: {state.value.errorMessage}
                </div>
            </template>

            <template lwc:elseif={state.value.accountInfo}>
                <dl class="slds-list_horizontal slds-wrap">
                    <dt class="slds-item_label slds-text-color_weak slds-truncate">取引先名:</dt>
                    <dd class="slds-item_detail slds-truncate">{state.value.accountInfo.name}</dd>
                    
                    <dt class="slds-item_label slds-text-color_weak slds-truncate">業種:</dt>
                    <dd class="slds-item_detail slds-truncate">{state.value.accountInfo.industry}</dd>
                    
                    <dt class="slds-item_label slds-text-color_weak slds-truncate">電話番号:</dt>
                    <dd class="slds-item_detail slds-truncate">{state.value.accountInfo.phone}</dd>
                </dl>
                
                <div class="slds-m-top_medium">
                    <lightning-button label="別のレコードをロード" onclick={handleReloadAnother}></lightning-button>
                </div>
            </template>

        </div>
    </lightning-card>
</template>

アーキテクチャの解説とメリット

この設計(ネストされた状態マネージャー)がもたらす最大の利点は、「関心の完全な分離(Separation of Concerns)」です。

  1. データ取得の抽象化
    UIコンポーネントは、背後でLDSが動いているのか、UI APIが呼ばれているのかを一切知る必要がありません。ただstatusloadedになったかどうかを監視し、データを表示するだけです。
  2. インテリジェントなリアクティビティ
    組み込みのsmRecordに渡す設定(recordConfig)自体をatomで定義したことで、UIからアクション(loadNewAccount)を呼び出してatomを更新するだけで、連鎖的に新しいレコードのデータフェッチが走ります。
  3. 複数データソースの統合
    今回はsmRecord一つでしたが、要件によってはsmLayoutを同時に組み込んだり、複数のレコードデータを一つのcomputed内で結合して「アプリケーションに必要な完全なデータモデル」を構築してからUIに渡すことも可能です。

Lightning Experienceでの動作確認(レコードページへの配置)

今回の accountViewer コンポーネントは、現在表示している取引先のレコードID(@api recordId)を初期状態として受け取るように設計されています。そのため、取引先のレコードページに配置して動作を確認します。

  1. ページへの配置: プロジェクトを組織にデプロイした後、Salesforce上で任意の「取引先」のレコード詳細画面を開き、コンポーネントを配置します。
  2. 初期ロード時の動作: レコード画面に戻ると、配置したコンポーネントが現在の取引先IDを自動的に取得して状態マネージャーに渡します。状態マネージャー内部の smRecord がデータを取得し、ローディングスピナーが消えた後に取引先名や業種が画面に表示されます。
  3. 動的な再フェッチの確認: コンポーネント内の「別のレコードをロード」ボタンをクリックしてみてください。UI側からのアクション呼び出しにより、状態マネージャー内部の recordConfig(設定用atom)の recordId が書き換わります。すると、組み込み状態マネージャーがその変更を自動的に検知して新たなレコードデータへと動的に再フェッチを行い、画面をシームレスに更新します。

このように、UIコンポーネントは単に「初期IDを渡す」「アクションを呼ぶ」だけであり、データフェッチやリアクティビティの複雑な制御はすべて状態マネージャー内に美しくカプセル化されていることが実感できるはずです。

別のレコードをロードボタンを押すと…
sForceの情報を取得して表示しました

連載のまとめ:LWCアーキテクチャのパラダイムシフト

全3回にわたり、ベータ版として提供されている@lwc/stateの全貌を解説してきました。

  1. 第1回では、atomcomputedを用いて、散らかりがちなコンポーネント内の状態を中央集権化する基礎を学びました。
  2. 第2回では、fromContextによるContextパターンを用いて、プロップスドリリング(バケツリレー)を排除し、複数コンポーネントで単一の情報を同期する実践的なウィザードを構築しました。
  3. 第3回(今回)では、組み込み状態マネージャーをネストさせ、@wireに依存せずにSalesforceデータと密接に連携するページレベルの堅牢なデータ層を設計しました。

従来のLWC開発では、「表示コンポーネント」が自らデータを取りに行き、自らロジックを計算し、他のコンポーネントにイベントで通知するというように、一つのコンポーネントが多くの責務を抱え込みすぎていました。 @lwc/stateの導入は、こうしたモノリシック(一枚岩)なコンポーネント設計から脱却し、最新のフロントエンド標準に沿ったスケーラブルなアーキテクチャへの進化をSalesforce開発者にもたらします。

複雑なエンタープライズ要件に直面した際は、ぜひこの新しい状態管理のアプローチを取り入れてみてください。

参考URL

Manage State Across LWC Components with State Managers (Beta)

DXforceの管理人

福島 瑛二

2013年にJavaエンジニアとしてのキャリアをスタート。2019年にSalesforceと出会い、Salesforceエンジニアの道へ。

デザインや UI/UX の観点からもシステムを捉え、ユーザーにとって心地よい体験を実装することにやりがいを感じています。

CRM(顧客データ)や Data Cloud と連携した高度なサイトを目に見える形で表現できる Experience Cloud に大きな可能性を見出しており、バックエンドのデータ構造とフロントエンドの表現力を極めることがこれからの Salesforce エンジニアに求められるスキルだと確信しています。

Trailblazer: efukushima

福島 瑛二をフォローする

読者の声

タイトルとURLをコピーしました