coordinate-components について
Blazor のコンポーネント間で状態を共有する方法の選択と実装を支援するスキルです。カスケード値、DI 登録のカスケード値ソース、スコープサービスに加え、レンダーモード境界や変更通知も扱います。
主な機能
- 01状態の範囲に応じた共有方式の選択
- 02カスケード値による部分ツリー状態の共有
- 03カスケード値ソースによるアプリ全体への変更通知
- 04スコープサービスによる可変状態の管理
- 05レンダーモードとサービスライフタイムへの対応
活用シーン
- 01コンポーネントツリー内でテーマやレイアウト設定を共有する
- 02レンダーモードをまたいでユーザー情報や機能フラグを共有する
- 03カート、通知数、フィルターを管理する
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dotnet/ skills --skill coordinate-componentsインストール後、Claude Code で/coordinate-componentsを入力して読み込まれたか確認します。
詳細
コンポーネント間で状態を共有する
ステップ 1 — AGENTS.md を読む
変更を加える前に、ワークスペースのルートにある AGENTS.md を読み、プロジェクトの規約を確認します。
ステップ 2 — 適用範囲を決める
| 必要な範囲 | 仕組み | 使用する状況 |
|---|---|---|
| サブツリー(同じレンダーモード) | CascadingValue コンポーネント | レイアウト内のテーマやレイアウト設定 |
| アプリ全体(すべてのレンダーモード) | DI 経由の CascadingValueSource<T> | 現在のユーザー、機能フラグ、グローバルに共有するテーマ |
| サーキット内の変更可能な共有状態 | スコープ付きサービス + Action イベント | ショッピングカート、通知数、選択したフィルター |
親から子へ 1 階層だけ渡す場合は、[Parameter] / EventCallback を使用します(author-component スキルを参照)。
プリレンダリングからインタラクティブへの移行をまたいで状態を保持する場合は、support-prerendering スキルを参照してください。
ワークフロー(クイックリファレンス)
- ステップ 2 の表から仕組みを選択する
- レンダーモードの境界をまたぐ場合 →
CascadingValueSource<T>を使用する(ステップ 4) Program.csでAddCascadingValue(...)とisFixed: falseを指定して登録する- 子コンポーネントで
[CascadingParameter]を使って受け取る NotifyChangedAsync(newValue)で更新する — ページを再読み込みしない- サーキット内に追加の変更可能な状態が必要な場合 → スコープ付きサービスを追加する(ステップ 5)
- バックグラウンドスレッドからの
StateHasChangedは、InvokeAsyncでラップする IDisposableを実装する — タイマーの破棄、トークンのキャンセル、イベントの購読解除を行う
ステップ 3 — サブツリーの状態に対する CascadingValue
サブツリーを <CascadingValue> でラップすると、すべての中間コンポーネントを経由して渡さなくても、すべての子孫コンポーネントにデータを渡せます。
@* In a layout or parent component *@
<CascadingValue Value="theme">
@Body
</CascadingValue>
@code {
private ThemeInfo theme = new() { ButtonClass = "btn-primary" };
}
任意の子孫コンポーネントで受け取ります。
[CascadingParameter]
private ThemeInfo? Theme { get; set; }
ルール:
- 名前ではなく、型で照合されます。同じ型の値を複数カスケードするには、
Nameを追加します。<CascadingValue Value="primary" Name="PrimaryTheme">...</CascadingValue>[CascadingParameter(Name = "PrimaryTheme")] private ThemeInfo? Primary { get; set; } - 値が変わらない場合は
IsFixed="true"を設定します。これにより、サブスクリプションのオーバーヘッドを回避できます。 - レンダーモードの境界をまたぎません。 静的 SSR の親にある
<CascadingValue>は、インタラクティブな子コンポーネントからは見えません。ステップ 6 を参照してください。
ステップ 4 — アプリ全体の状態に対する CascadingValueSource<T>
レンダーモードにかかわらずすべてのコンポーネントで値を使用できる必要がある場合は、DI に CascadingValueSource<T> を登録します。
// Program.cs
builder.Services.AddCascadingValue(sp =>
{
var theme = new ThemeInfo { ButtonClass = "btn-primary" };
return new CascadingValueSource<ThemeInfo>(theme, isFixed: false);
});
ステップ 3 と同じ方法で受け取ります。
[CascadingParameter]
private ThemeInfo? Theme { get; set; }
購読者を更新して通知するには、既存のオブジェクトを変更するか、新しいオブジェクトに置き換えます。
@* Component that changes the theme *@
@inject CascadingValueSource<ThemeInfo> ThemeSource
<button @onclick="ToggleDarkMode">Toggle theme</button>
@code {
private bool isDark;
private async Task ToggleDarkMode()
{
isDark = !isDark;
// Replace the value entirely:
var newTheme = new ThemeInfo { ButtonClass = isDark ? "btn-dark" : "btn-primary" };
await ThemeSource.NotifyChangedAsync(newTheme);
}
}
引数なしの NotifyChangedAsync() も使用できます。オブジェクトを変更してから呼び出します。NotifyChangedAsync(newValue) は、値の置き換えと通知を一度に行います。
更新プロトコル: 共有状態が変わるたびに、変更を行うコンポーネントは CascadingValueSource<T> を必ずインジェクションし、NotifyChangedAsync() を呼び出す必要があります。これが、すべての [CascadingParameter] 購読者の再レンダリングをトリガーする唯一の仕組みです。この呼び出しがなければ、購読者は更新されません。代わりに NavigationManager.Refresh() やページの再読み込みを使用しないでください。
ルール:
isFixed: falseを指定すると、変更通知が有効になります。真に静的な値(機能フラグ)にはisFixed: trueが適しています。- レンダーモードの境界をまたぎます — ページ単位のインタラクティブ性、グローバルなインタラクティブ性、WebAssembly で動作します。
<CascadingValue>に対する主な利点です。 - カスケードする型は細かく分けてください。
NotifyChangedAsyncを呼び出すたびに、どのプロパティが変更されたかにかかわらず、すべての購読者が再レンダリングされます。アプリの状態すべてを 1 つのカスケード型にまとめないでください。 - Auto/WebAssembly アプリでは、サーバー側と
.ClientのProgram.csの両方に登録します。型は共有アセンブリに配置する必要があります。
ステップ 5 — 変更イベントを備えたスコープ付き状態サービス
複数のコンポーネントが読み取りと書き込みを行う共有の可変状態(ショッピングカート、通知数、フィルター)には、変更を通知するイベントを備えたスコープ付きサービスを使用します。
サービスを定義します:
public class CartState
{
private readonly List<CartItem> _items = [];
public IReadOnlyList<CartItem> Items => _items;
public int Count => _items.Count;
public event Action? OnChange;
public void Add(CartItem item)
{
_items.Add(item);
OnChange?.Invoke();
}
public void Remove(CartItem item)
{
_items.Remove(item);
OnChange?.Invoke();
}
}
スコープ付きとして登録します:
builder.Services.AddScoped<CartState>();
コンポーネントでサブスクライブします:
@inject CartState Cart
@implements IDisposable
<span class="badge">@Cart.Count</span>
@code {
protected override void OnInitialized()
{
Cart.OnChange += StateHasChanged;
}
public void Dispose()
{
Cart.OnChange -= StateHasChanged;
}
}
シンプルな Action OnChange パターンは、Blazor の同期コンテキストからイベントが発生する場合(ボタンクリック → Cart.Add(…))に機能します。イベントが同期コンテキスト外(タイマー、バックグラウンドタスク、SignalR ハブ)から発生する場合は、InvokeAsync でラップします:
private Action? _handler;
protected override void OnInitialized()
{
_handler = () => InvokeAsync(StateHasChanged);
Cart.OnChange += _handler;
}
public void Dispose() => Cart.OnChange -= _handler;
デリゲートをフィールドに保存して、まったく同じインスタンスのサブスクリプションを解除できるようにします。
ステップ 6 — レンダーモードとサービスのライフタイムに関するルール
カスケード値はレンダーモードの境界を越えません
静的 SSR レイアウト(レイアウトが静的にレンダリングされる場合の MainLayout.razor)に配置された <CascadingValue> は、インタラクティブな子コンポーネントには届きません。インタラクティブコンポーネントのカスケードパラメーターは null になります。
解決策: DI に登録した CascadingValueSource<T>(ステップ 4)またはスコープ付きサービス(ステップ 5)を使用します。DI サービスはコンポーネントツリーからではなく、回路ごとに解決されるため、どちらも境界を越えます。
Server と WebAssembly のサービスライフタイム
| ライフタイム | Server | WebAssembly |
|---|---|---|
| スコープ付き | 回路ごと(ユーザー接続ごと) | ブラウザータブごと |
| シングルトン | すべてのユーザーで共有 | ブラウザータブごと(安全) |
| 一時的 | インジェクションごとに新しいインスタンス | インジェクションごとに新しいインスタンス |
Server では、ユーザー固有の状態をシングルトンに保存しないでください。すべてのユーザーの回路が同じシングルトンを共有し、あるユーザーのカートが別のユーザーに漏れてしまいます。AddScoped<T>() を使用します。
WebAssembly ではシングルトンはタブごとに作成されるため安全です。ただし、Server と WebAssembly の両方で動作するコード(Auto モード)では、スコープ付きサービスを使用する必要があります。
プリレンダリングを伴う Auto/WebAssembly
状態サービスは .Client プロジェクトまたは共有アセンブリで定義する必要があります。サーバー専用の型は参照できません。両方の Program.cs ファイルでサービスを登録します。プリレンダリング中に作成された状態は、インタラクティブランタイムへの切り替え後も保持されません。状態を引き継ぐには、support-prerendering スキルの [PersistentState] パターンを使用します。
やってはいけないこと
- Server でユーザーごとの状態にシングルトンを使用しないでください — すべての回路で共有されるため、ユーザー間で状態が漏れます。
- アプリの状態をすべて 1 つのカスケードオブジェクトにまとめないでください —
NotifyChangedAsyncは変更のたびにすべてのサブスクライバーを再レンダリングします。懸念事項ごとに別の型(ThemeState、CartState、UserPreferences)に分けます。 - サブスクリプションの解除を忘れないでください — イベントをサブスクライブしているのに
Disposeを実装しないと、メモリリークが発生し、回路ごとに増えていきます。 - 静的レイアウト内の
<CascadingValue>がインタラクティブな子コンポーネントに届くと想定しないでください — レンダーモードの境界を越えません。DI に登録したCascadingValueSource<T>またはスコープ付きサービスを使用します。 - カスケード値の変更を反映するために
NavigationManager.Refresh(forceReload: true)を呼び出さないでください — 回路が破棄され、ページ全体が再読み込みされます。代わりにCascadingValueSource<T>をインジェクションし、NotifyChangedAsync(newValue)を呼び出して、ページを再読み込みせずにすべての[CascadingParameter]サブスクライバーへ更新を通知します。 - Blazor 以外のスレッドから
StateHasChangedを呼び出さないでください —InvokeAsyncでラップします。そうしないと、フレームワークはInvalidOperationException: The current thread is not associated with the Dispatcherをスローします。