{
  "schema": "https://ai-atoms.com/schemas/skill-v1.json",
  "type": "skill",
  "id": "skill/coordinate-components",
  "version": "1.0.1",
  "name": "coordinate-components",
  "description": "Share state between components that don't have a direct parent-child parameter relationship, using cascading values, scoped services with change events, or CascadingValueSource via DI. USE WHEN the user needs a CascadingParameter or CascadingValue that works across render mode boundaries, a shopping cart or notification count accessible from multiple pages, a theme or user preference cascaded app-wide, or when components in different parts of the tree must react when shared data changes. Also USE WHEN cascading values aren't reaching interactive children in per-page interactivity mode, or when the user needs to understand scoped vs singleton service lifetime for state on Blazor Server. DO NOT USE for direct parent-child parameter passing or EventCallback (see author-component), for persisting state across prerender-to-interactive transitions (see support-prerendering), or for service abstractions for data fetching in Auto/WebAssembly (see fetch-and-send-data).",
  "system_prompt_fragment": "# Coordinate Components\n\n## Step 1 — Read AGENTS.md\n\nRead `AGENTS.md` at the workspace root to learn the project's conventions before making changes.\n\n## Step 2 — Decide the scope\n\n| Need | Mechanism | When to use |\n|------|-----------|-------------|\n| Subtree (same render mode) | `CascadingValue` component | Theme, layout config within a layout |\n| App-wide (all render modes) | `CascadingValueSource<T>` via DI | Current user, feature flags, theme shared globally |\n| Mutable shared state within a circuit | Scoped service + `Action` event | Shopping cart, notification count, selected filters |\n\nFor parent→child one level: use `[Parameter]` / `EventCallback` (see `author-component` skill).\nFor persisting state across prerender→interactive: see `support-prerendering` skill.\n\n## Workflow (quick reference)\n\n1. Choose the mechanism from the table in Step 2\n2. If crossing render mode boundaries → use `CascadingValueSource<T>` (Step 4)\n3. Register in `Program.cs` with `AddCascadingValue(...)` and `isFixed: false`\n4. Consume via `[CascadingParameter]` in child components\n5. Update via `NotifyChangedAsync(newValue)` — never page reload\n6. For additional mutable state within a circuit → add scoped service (Step 5)\n7. Wrap any `StateHasChanged` from background threads in `InvokeAsync`\n8. Implement `IDisposable` — dispose timers, cancel tokens, unsubscribe events\n\n## Step 3 — CascadingValue for subtree state\n\nWrap a subtree with `<CascadingValue>` to flow data to all descendants without passing it through every intermediate component.\n\n```razor\n@* In a layout or parent component *@\n<CascadingValue Value=\"theme\">\n    @Body\n</CascadingValue>\n\n@code {\n    private ThemeInfo theme = new() { ButtonClass = \"btn-primary\" };\n}\n```\n\nConsume in any descendant:\n\n```csharp\n[CascadingParameter]\nprivate ThemeInfo? Theme { get; set; }\n```\n\n**Rules:**\n- Matched by **type**, not name. To cascade multiple values of the same type, add `Name`:\n  ```razor\n  <CascadingValue Value=\"primary\" Name=\"PrimaryTheme\">...</CascadingValue>\n  ```\n  ```csharp\n  [CascadingParameter(Name = \"PrimaryTheme\")]\n  private ThemeInfo? Primary { get; set; }\n  ```\n- Set `IsFixed=\"true\"` when the value never changes — avoids subscription overhead.\n- **Does NOT cross render mode boundaries.** A `<CascadingValue>` in a static SSR parent is invisible to interactive children. See Step 6.\n\n## Step 4 — CascadingValueSource&lt;T&gt; for app-wide state\n\nRegister a `CascadingValueSource<T>` in DI when the value must be available to **all components regardless of render mode**.\n\n```csharp\n// Program.cs\nbuilder.Services.AddCascadingValue(sp =>\n{\n    var theme = new ThemeInfo { ButtonClass = \"btn-primary\" };\n    return new CascadingValueSource<ThemeInfo>(theme, isFixed: false);\n});\n```\n\nConsume identically to Step 3:\n\n```csharp\n[CascadingParameter]\nprivate ThemeInfo? Theme { get; set; }\n```\n\n**To update and notify subscribers**, either mutate the existing object or replace it:\n\n```razor\n@* Component that changes the theme *@\n@inject CascadingValueSource<ThemeInfo> ThemeSource\n\n<button @onclick=\"ToggleDarkMode\">Toggle theme</button>\n\n@code {\n    private bool isDark;\n\n    private async Task ToggleDarkMode()\n    {\n        isDark = !isDark;\n        // Replace the value entirely:\n        var newTheme = new ThemeInfo { ButtonClass = isDark ? \"btn-dark\" : \"btn-primary\" };\n        await ThemeSource.NotifyChangedAsync(newTheme);\n    }\n}\n```\n\n`NotifyChangedAsync()` (no argument) also works — mutate the object and then call it. `NotifyChangedAsync(newValue)` replaces the value and notifies in one step.\n\n**Update protocol:** Whenever shared state changes, the component that changes it MUST inject `CascadingValueSource<T>` and call `NotifyChangedAsync()`. This is the only mechanism that triggers re-rendering in all `[CascadingParameter]` subscribers. Without this call, no subscribers update. Do not use `NavigationManager.Refresh()` or page reloads as a substitute.\n\n**Rules:**\n- `isFixed: false` enables change notifications. `isFixed: true` is better for truly static values (feature flags).\n- **Crosses render mode boundaries** — works for per-page interactivity, global interactivity, and WebAssembly. Key advantage over `<CascadingValue>`.\n- Keep cascaded types **granular**. Every `NotifyChangedAsync` re-renders ALL subscribers regardless of which property changed. Don't put all app state into one cascaded type.\n- For Auto/WebAssembly apps, register in **both** server and `.Client` `Program.cs`. The type must be in a shared assembly.\n\n## Step 5 — Scoped state service with change events\n\nFor mutable shared state that multiple components read **and write** (shopping cart, notification count, filters), use a scoped service with an event for change notification.\n\n**Define the service:**\n\n```csharp\npublic class CartState\n{\n    private readonly List<CartItem> _items = [];\n\n    public IReadOnlyList<CartItem> Items => _items;\n    public int Count => _items.Count;\n\n    public event Action? OnChange;\n\n    public void Add(CartItem item)\n    {\n        _items.Add(item);\n        OnChange?.Invoke();\n    }\n\n    public void Remove(CartItem item)\n    {\n        _items.Remove(item);\n        OnChange?.Invoke();\n    }\n}\n```\n\n**Register as scoped:**\n\n```csharp\nbuilder.Services.AddScoped<CartState>();\n```\n\n**Subscribe in components:**\n\n```razor\n@inject CartState Cart\n@implements IDisposable\n\n<span class=\"badge\">@Cart.Count</span>\n\n@code {\n    protected override void OnInitialized()\n    {\n        Cart.OnChange += StateHasChanged;\n    }\n\n    public void Dispose()\n    {\n        Cart.OnChange -= StateHasChanged;\n    }\n}\n```\n\nThe simple `Action OnChange` pattern works when the event fires from the Blazor sync context (button click → `Cart.Add(…)`). If the event fires from **outside** the sync context (timer, background task, SignalR hub), wrap in `InvokeAsync`:\n\n```csharp\nprivate Action? _handler;\n\nprotected override void OnInitialized()\n{\n    _handler = () => InvokeAsync(StateHasChanged);\n    Cart.OnChange += _handler;\n}\n\npublic void Dispose() => Cart.OnChange -= _handler;\n```\n\nStore the delegate in a field so you can unsubscribe the exact same instance.\n\n## Step 6 — Render mode and service lifetime rules\n\n### Cascading values don't cross render mode boundaries\n\nA `<CascadingValue>` placed in a static SSR layout (`MainLayout.razor` when the layout renders statically) will **not** reach interactive children. The interactive component sees `null` for the cascading parameter.\n\n**Fix:** Use `CascadingValueSource<T>` registered in DI (Step 4) or a scoped service (Step 5). Both cross boundaries because DI services are resolved per-circuit, not from the component tree.\n\n### Service lifetime on Server vs WebAssembly\n\n| Lifetime | Server | WebAssembly |\n|----------|--------|-------------|\n| **Scoped** | Per circuit (per user connection) | Per browser tab |\n| **Singleton** | Shared across ALL users | Per browser tab (safe) |\n| **Transient** | New instance per injection | New instance per injection |\n\nOn Server, **never store user-specific state in a singleton** — every user's circuit shares the same singleton. One user's cart leaks into another's. Use `AddScoped<T>()`.\n\nOn WebAssembly, singletons are per-tab and safe. But code meant for **both** Server and WebAssembly (Auto mode) must use scoped.\n\n### Auto/WebAssembly with prerendering\n\nState services must be defined in the `.Client` project or a shared assembly — they cannot reference server-only types. Register the service in both `Program.cs` files. State created during prerender does not survive the switch to the interactive runtime. Use the `support-prerendering` skill's `[PersistentState]` pattern to carry state across.\n\n## Don'ts\n\n- **Don't use a singleton for per-user state on Server** — all circuits share it, leaking state between users.\n- **Don't put all app state into one cascaded object** — `NotifyChangedAsync` re-renders ALL subscribers on every change. Separate concerns into distinct types (`ThemeState`, `CartState`, `UserPreferences`).\n- **Don't forget to unsubscribe** — omitting `Dispose` on event subscriptions causes memory leaks that grow per-circuit.\n- **Don't use `<CascadingValue>` in a static layout expecting it to reach interactive children** — it won't cross render mode boundaries. Use DI-registered `CascadingValueSource<T>` or scoped services.\n- **Don't use `NavigationManager.Refresh(forceReload: true)` to propagate cascading value changes** — this destroys the circuit and forces a full page reload. Instead, inject `CascadingValueSource<T>` and call `NotifyChangedAsync(newValue)` to push updates to all `[CascadingParameter]` subscribers without a page reload.\n- **Don't call `StateHasChanged` from a non-Blazor thread** — wrap in `InvokeAsync`. The framework throws `InvalidOperationException: The current thread is not associated with the Dispatcher`.",
  "applicable_domains": [
    "code",
    "dotnet",
    "engineering"
  ],
  "invocation": [
    "/coordinate-components"
  ],
  "tags": [
    "dotnet-blazor",
    "dotnet",
    "csharp",
    "microsoft"
  ],
  "authored_by": "anthropics",
  "source_url": "https://github.com/dotnet/skills/blob/main/plugins/dotnet-blazor/skills/coordinate-components/SKILL.md",
  "lifecycle": "stable",
  "category": "dotnet",
  "provenance": {
    "source": "dotnet/skills",
    "source_url": "https://github.com/dotnet/skills/blob/main/plugins/dotnet-blazor/skills/coordinate-components/SKILL.md",
    "author": "Microsoft / .NET Foundation",
    "license": "MIT",
    "notes": "Imported by scripts/import-anthropic-skills.py."
  }
}