はじめに
LWCで画面を組んでいて、propsのバケツリレーとCustomEventの管理に時間を取られていませんか。
Summer '26でGAになった@lwc/stateを使うと、defineStateとfromContextの2つのAPIだけでコンポーネント間の状態共有を実装できます。
この記事ではコード例をベースに、atom・computed・setAtomの使い分けと、Salesforceの組み込みState Manager(lightning/stateManagerRecord)を使った実装パターンまで一気にまとめます。
LWC開発でstate管理に悩んでいるエンジニアの方は参考にしてください。
バケツリレーがつらい理由を整理する
まず、なぜpropsとイベントのやり取りがつらくなるのか、ここを整理しておきます。
たとえば商品一覧を表示するコンポーネントと、カート合計金額を表示するコンポーネントが並んでいる画面を考えてみてください。商品一覧側で「カートに追加」ボタンを押すと、CustomEventを発火してカート合計側に伝える必要があります。この2つが直接の親子関係ならまだいいのですが、間に別のコンポーネントを挟んだ瞬間、イベントをバケツリレーで中継しないといけなくなります。
Excelでいうと、シート同士の値を直接参照せず、わざわざ手入力でコピペして回っているような状態です。データが1箇所にまとまっていないので、どこで何が更新されたのか追いにくくなります。
@lwc/stateが解決するのは、まさにこの「データの置き場所が分散してしまう問題」です。状態を1箇所の「State Manager」にまとめておき、それを参照したいコンポーネントがそこから直接値を取りに行く形に変えます。
つまり、バケツリレーの原因は「データの置き場所がバラバラなこと」で、State Managerはその置き場所を1つにまとめる仕組みだ、という理解でOKです。

先につまずきポイントを共有しておきます
実装の話に入る前に、実際に触ってみて引っかかった点を先に共有しておきます。手順を追う前に知っておいたほうが、無駄な時間を使わずに済むはずです。
1つ目は、Experience Cloudではまだ使えないという点です。
Salesforce公式のブログでも「Lightning Experienceでは使えるが、Experience Cloudにはまだ対応していない」と明記されています。importの記述自体はエディタ上でエラーにならないので、コミュニティサイトのコンポーネントに組み込んでから動かないことに気づく、というのはやりがちな失敗だと思います。
着手する前に、対象がLightning Experience向けなのかコミュニティ向けなのかを確認しておくべきです。
2つ目は、LWCのAPIバージョンです。
@lwc/stateはSummer '26に対応するLWC API v67.0以降のコンポーネントで使う機能です。既存プロジェクトで試す場合は、対象コンポーネントのjs-meta.xmlで指定しているAPIバージョンを確認してください。
古いバージョンのままだと、パッケージだけ更新してもimportの時点で動きません。手元の環境でも、既存コンポーネントを流用しようとして最初にここでつまずきました。バージョンを上げる際は、@lwc/state以外の破壊的変更もまとめて影響を受けるので、sandboxで一通り動作確認してから本番に反映することをおすすめします。
3つ目は、追加費用についてです。
今回調査した範囲では、@lwc/stateはLightning Platformの標準機能として提供されており、別途アドオンの購入が必要という情報は見当たりませんでした。ライセンス周りの最終確認は、実際に組織へ適用する前に必ず公式ドキュメントで確認してください。
defineStateでState Managerを作る
実際のコードを見てみます。State Managerは@lwc/stateが提供するdefineStateという関数で作ります。
たとえば、商談一覧をキーワードで絞り込むフィルタを例にします。
import { defineState } from '@lwc/state';
export const createOpportunityFilter = defineState(({ atom, computed, setAtom }, opportunities = []) => {
const keyword = atom('');
const filtered = computed([keyword], (word) =>
opportunities.filter((opp) => opp.Name.toLowerCase().includes(word.toLowerCase()))
);
const search = (word) => setAtom(keyword, word);
return { keyword, filtered, search };
});
出てくる要素は3つだけです。
atomは状態そのものを持つ入れ物、
computedはatomから自動で計算される値、
setAtomは値を更新するときに使う関数です。
正直、初めてこのコードを見たときは「atomって何?」と身構えました。でも実態はシンプルで、値をラップして変更を検知できるようにしただけの箱だと思えば十分です。
searchを呼ぶとkeywordが変わり、keywordを参照しているfilteredも自動で再計算されます。コンポーネント側で明示的に再描画の指示を書く必要はありません。
つまり、値の入れ物と、それを更新する関数をワンセットで定義するのがState Managerだ、という理解でOKです。

fromContextで複数コンポーネントに共有する
State Managerを作っただけでは、まだ1つのコンポーネントの中で完結しています。複数のコンポーネントで同じ状態を共有するにはfromContextを使います。
まず、ツリーの上位にあたるコンポーネントでインスタンスを作ります。ここでは画面全体で使うトースト通知の状態を例にします。
import createToastState from 'x/toastState';
export default class AppShell extends LightningElement {
toastState = createToastState();
}
その配下にある子コンポーネント側では、propsを一切受け取らずに同じインスタンスを参照できます。
import { LightningElement } from 'lwc';
import { fromContext } from '@lwc/state';
import createToastState from 'x/toastState';
export default class RecordSaveButton extends LightningElement {
toastState = fromContext(createToastState);
handleSave() {
// 保存処理のあと、祖先コンポーネントを何段も経由せず直接通知を出せる
this.toastState.value.notify('保存しました');
}
}
実務で使うなら、ここが一番のメリットだと感じています。深い階層にある保存ボタンから、離れた場所にあるトースト表示コンポーネントへ直接状態を届けられるので、親から子へ何段も@apiプロパティをバケツリレーする必要がありません。
Salesforceのレコードデータを扱う場合は、自分でState Managerを一から作らなくても、lightning名前空間に組み込みのState Managerが用意されています。lightning/stateManagerRecordを使えば、Lightning Data Serviceの仕組みに乗ったまま、レコードの取得状態(loading・loaded・errorなど)を含めて扱えます。
つまり、propsもイベントも書かずにコンポーネント同士がデータを共有できる、という理解でOKです。自作と組み込み、両方のState Managerを組み合わせれば、画面全体のデータフローを1つのパターンで統一できます。

まとめ
今回の内容を整理します。
- Summer '26で
@lwc/stateが正式GAになり、LWCのコンポーネント間で状態を共有する標準パターンが手に入った -
defineStateで状態(atom)と、そこから計算される値(computed)、更新用の関数をひとまとめに定義する -
fromContextを使えば、propsやCustomEventのバケツリレーなしに複数コンポーネントで同じ状態を参照できる - Experience Cloudでは未対応、LWC API v67.0以降が必要という制約は事前に押さえておく
State Managerというパターン自体は新しい考え方なので、慣れるまで少し時間がかかると思います。私もまだ手探りの部分がありますが、一緒に少しずつ慣れていきましょう!
出典:LWC State Managers: Share Reactive State Across Components | Salesforce Developers Blog
現場での気づきをnoteで発信しています。
→ note