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

【@lwc/state連載】第2回:コンポーネント間の状態共有(Contextパターンの実践)

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

第1回では、@lwc/stateの基本となる4つのプリミティブ(defineStateatomcomputedsetAtom)を用いて、単一のコンポーネント内でUIとビジネスロジックを分離する方法を学びました。

しかし、実際のエンタープライズ開発において、UIが単一のコンポーネントで完結することは稀です。例えば「入力ステップ」「確認ステップ」「サマリー表示」のように複数のコンポーネントが組み合わさったウィザード画面を構築する場合、従来は以下のような複雑なデータの受け渡しが必要でした。

  1. 親コンポーネントが状態を@trackで保持する。
  2. 親から子コンポーネントへ@apiプロパティでデータを渡す(プロップスドリリング)。
  3. 子コンポーネントで入力が変更されたら、CustomEventを発行して親に通知する。
  4. 親がイベントを受け取り、状態を更新して再び子へプロパティを流し込む。

このような密結合な設計は、コンポーネントの階層が深くなるほど保守を困難にします。一方で、Lightning Message Service (LMS) はパブリッシュ/サブスクライブモデルを提供しますが、これは「別々のページや、DOMツリー上で無関係なコンポーネント間」での疎結合な通信を想定したものであり、ひとつのウィザードのような密接したコンポーネント群での厳密な状態共有に多用すべきではありません

ここで登場するのが、@lwc/stateが提供する真骨頂である 「プロバイダー/コンシューマーパターン(Contextパターン)」 です

DXforce Point

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

DXforce Point for Developers

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

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

fromContextによる状態の階層的な共有

@lwc/stateでは、親から子へバケツリレーのようにデータを渡す必要はありません。DOMツリーの上位にあるコンポーネントが「プロバイダー(提供者)」となり、配下の子孫コンポーネント(コンシューマー)は fromContext APIを使うことで、どこからでも直接その状態にアクセスできるようになります

厳格なライフサイクルのルール

この強力な機能を安全に使うためには、LWCのDOM接続ライフサイクルに基づいた絶対に守るべきルールがあります

  • プロバイダー(親)のルール: コンポーネントがDOMに接続される前(初期化時)に、状態マネージャーのインスタンスを生成して自身のプロパティに保持しなければなりません。
  • コンシューマー(子孫)のルール: fromContext を宣言しても、コンポーネントがDOMに挿入される(connectedCallback が実行される)までは、状態マネージャーの実体を受け取ることができません。そのため、コンストラクタ(constructor)の内部で this.state.value のように値を参照しようとすると致命的なエラーになります

実践:Lightning Experienceで動くウィザード画面

百聞は一見に如かず。Lightning Experienceのアプリケーションページに配置して動かせる、複数コンポーネントにまたがる「ユーザー情報ウィザード」を作成してみましょう。

アーキテクチャ解説

下の図は、@lwc/state の「プロバイダー/コンシューマーパターン」による状態共有の仕組みを視覚化したものです。

  1. 状態の提供(プロバイダー): wizardContainer は親コンポーネントとして、初期化時に状態マネージャーStateManagerのインスタンスを作成し、自身と配下の子孫コンポーネントに対して提供可能な状態(コンテキスト)として保持します。
  2. 状態の解決(コンシューマー): wizardInputwizardSummary はDOMに接続される際、fromContext を使用してDOMツリーを上方へ遡り、最も近いプロバイダー(この場合は wizardContainer)が保持している状態インスタンスの参照を取得します。
  3. アクションの実行とリアクティビティ: wizardInput 側で文字が入力されアクションが実行されると、状態マネージャー内の中央集権化された状態(atom)が更新されます。この変更はそれを監視しているすべてのコンポーネントや派生状態(computed)に即座に通知されるため、離れた場所にある wizardSummary も自動的かつ同期的に再レンダリングされます。

フォルダ構成

今回は4つのLWCを作成します。大元のコンテナ(親)と、入力用・表示用の2つの子コンポーネント、そして状態管理モジュールです。

force-app/main/default/lwc/
├── wizardStateManager/ <-- 状態管理APIモジュール (UIなし)
├── wizardContainer/ <-- 親コンポーネント (プロバイダー/画面配置用)
├── wizardInput/ <-- 子コンポーネントA (入力用コンシューマー)
└── wizardSummary/ <-- 子コンポーネントB (表示用コンシューマー)

状態管理モジュール (wizardStateManager)

すべてのコンポーネントで共有される「信頼できる単一の情報源」を定義します。
(※ js-meta.xmlisExposedfalse にします。)

wizardStateManager.js

import { defineState } from '@lwc/state';

export default defineState(({ atom, computed, setAtom }) => {
    // 1. Atoms (入力データを保持)
    const firstName = atom('');
    const lastName = atom('');

    // 2. Computed (派生状態を自動計算)
    const fullName = computed([firstName, lastName], (first, last) => {
        if (first && last) return `${last} ${first}`;
        if (first || last) return first || last;
        return '未入力';
    });

    // 3. Actions (状態を安全に更新)
    const updateFirstName = (val) => setAtom(firstName, val);
    const updateLastName = (val) => setAtom(lastName, val);

    return {
        firstName,
        lastName,
        fullName,
        updateFirstName,
        updateLastName
    };
});

親コンポーネント / プロバイダー (wizardContainer)

このコンポーネントが状態のインスタンスを作成し、子コンポーネントを内包します。ここで作成された状態が、ツリーの下位に提供されます

wizardContainer.js

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

export default class WizardContainer extends LightningElement {
    // インスタンスを生成し、プロパティに割り当てる (ここでプロバイダーとなる)
    @api state = wizardStateManager();
}

wizardContainer.html

<template>
    <lightning-card title="マルチコンポーネント状態共有デモ" icon-name="standard:user">
        <div class="slds-grid slds-gutters slds-p-around_medium">
            <div class="slds-col slds-size_1-of-2">
                <c-wizard-input></c-wizard-input>
            </div>
            <div class="slds-col slds-size_1-of-2">
                <c-wizard-summary></c-wizard-summary>
            </div>
        </div>
    </lightning-card>
</template>

wizardContainer.js-meta.xml

画面に配置できるように、この大元のコンテナのみ isExposedtrue にします。

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>66.0</apiVersion>
    <isExposed>true</isExposed>
    <masterLabel>Wizard State Container</masterLabel>
    <targets>
        <target>lightning__AppPage</target>
        <target>lightning__RecordPage</target>
        <target>lightning__HomePage</target>
    </targets>
</LightningComponentBundle>

子コンポーネントA / 入力用 (wizardInput)

親からプロパティを受け取るのではなく、fromContext を使って直接状態を取りに行きます。
(※ js-meta.xmlisExposedfalse にします。)

wizardInput.js

import { LightningElement } from 'lwc';
import { fromContext } from '@lwc/state';
import wizardStateManager from 'c/wizardStateManager';

export default class WizardInput extends LightningElement {
    // DOMツリーを遡って、親のwizardContainerが持つインスタンスを取得する
    form = fromContext(wizardStateManager);

    handleFirstName(event) {
        this.form.value.updateFirstName(event.target.value);
    }

    handleLastName(event) {
        this.form.value.updateLastName(event.target.value);
    }
}

wizardInput.html

<template>
    <div class="slds-box">
        <h3 class="slds-text-heading_small slds-m-bottom_small">入力エリア</h3>
        <lightning-input label="姓" value={form.value.lastName.value} onchange={handleLastName}></lightning-input>
        <lightning-input label="名" value={form.value.firstName.value} onchange={handleFirstName}></lightning-input>
    </div>
</template>

子コンポーネントB / 表示用 (wizardSummary)

入力用コンポーネントとは完全に独立していますが、同じく fromContext を使って状態を参照するだけで、リアルタイムに同期されます。
(※ js-meta.xmlisExposedfalse にします。)

wizardSummary.js

import { LightningElement } from 'lwc';
import { fromContext } from '@lwc/state';
import wizardStateManager from 'c/wizardStateManager';

export default class WizardSummary extends LightningElement {
    form = fromContext(wizardStateManager);
}

wizardSummary.html

<template>
    <div class="slds-box slds-theme_shade">
        <h3 class="slds-text-heading_small slds-m-bottom_small">リアルタイムプレビュー</h3>
        
        <p><strong>フルネーム (自動計算):</strong></p>
        <p class="slds-text-heading_medium slds-text-color_success">
            {form.value.fullName.value}
        </p>
    </div>
</template>

画面での実際の動き

コードを組織にデプロイし、[設定] から「Lightning アプリケーションビルダー」を開きます。任意のアプリケーションページやホームページを作成(または編集)し、左側のコンポーネントパネルから「Wizard State Container」を画面にドラッグ&ドロップして保存します。

画面を開くと、左側の「入力エリア」と右側の「プレビュー」が並んで表示されます。 入力エリアに文字を打ち込むと、CustomEventを一切発行していないにも関わらず、右側のプレビュー画面にあるフルネーム(computedによる自動計算結果)がリアルタイムに更新されます。

子コンポーネント(wizardInputwizardSummary)は、お互いの存在を全く知りません。親コンポーネント(wizardContainer)も、単に枠組みを用意しただけで、データの橋渡しをするような冗長なJavaScriptコードは1行も書いていません。すべてのビジネスロジックと状態管理は wizardStateManager に中央集権化されています。

初期表示
入力時にリアルタイムで右側エリアの表示が動いていく
再掲

第2回のまとめと次回予告

fromContext を利用したプロバイダー/コンシューマーパターンにより、複雑にネストされたコンポーネントツリーにおける状態共有の課題が劇的にシンプルに解決できることがお分かりいただけたかと思います。このアプローチは、多段階の登録ウィザードや、複数のタブで構成された複雑な設定画面を構築する際に最大の威力を発揮します。

さて、エンタープライズアプリケーションにおいては、単に画面内の状態を管理するだけでなく「Salesforceのレコードデータやレイアウトメタデータ」を扱いたい場面が大半です。

次回、最終回となる「第3回:組み込み状態マネージャーと高度な統合パターン」では、Salesforceが標準で提供する stateManagerRecordstateManagerLayout をカスタム状態マネージャーと組み合わせ、ApexやWireに依存せずに複雑なデータフェッチと状態管理を統合する高度なテクニックを解説します。お楽しみに!

参考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をコピーしました