Share state between Blazor components using cascading values, scoped services, or CascadingValueSource via dependency injection.
- GitHub stars
- 5,942
- Files
- 1
- Last commit
- Nov 11, 2026
- License
- MIT
- Allowed tools
- —
What does the coordinate-components skill do?
coordinate-components is an agent skill. Share state between Blazor components using cascading values, scoped services, or CascadingValueSource via dependency injection.
Summary generated from the skill's SKILL.md.
About the coordinate-components skill
Managing shared state between Blazor components that lack a direct parent-child relationship is the primary function of this skill. It provides developers with specific architectural patterns to synchronize data across different parts of an application, such as updating a shopping cart count or applying a global theme. The skill outlines how to use CascadingValue for subtrees within the same render mode, CascadingValueSource via dependency injection for app-wide state that crosses render mode boundaries, and scoped services with Action events for mutable state within a circuit.
When an agent uses this skill, it helps implement correct registration in Program.cs using methods like AddCascadingValue or AddScoped. It ensures that updates are handled through NotifyChangedAsync rather than page reloads and provides guidance on using InvokeAsync when dealing with background threads. This approach is specifically designed for complex Blazor scenarios where standard parameter passing is insufficient, while explicitly avoiding patterns meant for direct parent-child communication or simple data fetching.
What an agent can do with it
- 1Synchronize app-wide theme settingsImplement a global theme that remains accessible to all components regardless of their specific render mode.
- 2Pass data to component subtreesFlow configuration data down to all descendants within a specific layout or component branch.
Written from the skill's SKILL.md. Every use case is checked against a quote from it.
How to install the coordinate-components skill
Each command copies the skill folder into the client's skills directory. Restart the client or start a new session to load it.Install the coordinate-components skill in Plugin
/plugin marketplace add dotnet/skills /plugin install dotnet-blazor@dotnet-agent-skills
Install the coordinate-components skill in Claude Code
~/.claude/skills/coordinate-components/SKILL.md
$ npx degit dotnet/skills/plugins/dotnet-blazor/skills/coordinate-components ~/.claude/skills/coordinate-componentsInstall the coordinate-components skill in Claude Code (project)
.claude/skills/coordinate-components/SKILL.md
$ npx degit dotnet/skills/plugins/dotnet-blazor/skills/coordinate-components .claude/skills/coordinate-componentsInstall the coordinate-components skill in Codex
~/.agents/skills/coordinate-components/SKILL.md
$ npx degit dotnet/skills/plugins/dotnet-blazor/skills/coordinate-components ~/.agents/skills/coordinate-componentsInstall the coordinate-components skill in Gemini CLI
~/.gemini/skills/coordinate-components/SKILL.md
$ npx degit dotnet/skills/plugins/dotnet-blazor/skills/coordinate-components ~/.gemini/skills/coordinate-componentsInstall the coordinate-components skill in Claude.ai
npx degit dotnet/skills/plugins/dotnet-blazor/skills/coordinate-components coordinate-components && zip -r coordinate-components.zip coordinate-components # claude.ai → Customize → Skills → + → Create skill → Upload a skill → coordinate-components.zip
Automated checks
Ran Nov 15, 2026Full report →Passed our automated checks. That is not a security audit: read the scripts before you let an agent run them. How checks work
- Source repository linked and reachabledotnet/skills/plugins/dotnet-blazor/skills/coordinate-components is public and reachablegithub.com/dotnet/skills/tree/HEAD/plugins/dotnet-blazor/ski ↗Pass
- Open source license (OSI)MITspdx.org/licenses/MIT.html ↗Pass
- Commit or release in the last yearLast commit 2026-11-11github.com/dotnet/skills/commits ↗Pass
- No download-and-run or credential theft in scriptsNo scripts in the skill folderPass
- No hidden instructions in SKILL.mdNo override phrases, safeguard switches, credential requests or hidden charactersgithub.com/dotnet/skills/blob/HEAD/plugins/dotnet-blazor/ski ↗Pass
- Pre-approved tools are scopedNo pre-approved tools; the agent asks before using onegithub.com/dotnet/skills/blob/HEAD/plugins/dotnet-blazor/ski ↗Pass
- SKILL.md follows the Agent Skills formatValid name and description; says when to use it; 236 linesgithub.com/dotnet/skills/blob/HEAD/plugins/dotnet-blazor/ski ↗Pass
SKILL.md
View on GitHubCoordinate Components
Step 1 — Read AGENTS.md
Read AGENTS.md at the workspace root to learn the project's conventions before making changes.
Step 2 — Decide the scope
| Need | Mechanism | When to use |
|---|---|---|
| Subtree (same render mode) | CascadingValue component |
Theme, layout config within a layout |
| App-wide (all render modes) | CascadingValueSource<T> via DI |
Current user, feature flags, theme shared globally |
| Mutable shared state within a circuit | Scoped service + Action event |
Shopping cart, notification count, selected filters |
For parent→child one level: use [Parameter] / EventCallback (see author-component skill).
For persisting state across prerender→interactive: see support-prerendering skill.
Workflow (quick reference)
- Choose the mechanism from the table in Step 2
- If crossing render mode boundaries → use
CascadingValueSource<T>(Step 4) - Register in
Program.cswithAddCascadingValue(...)andisFixed: false - Consume via
[CascadingParameter]in child components - Update via
NotifyChangedAsync(newValue)— never page reload - For additional mutable state within a circuit → add scoped service (Step 5)
- Wrap any
StateHasChangedfrom background threads inInvokeAsync - Implement
IDisposable— dispose timers, cancel tokens, unsubscribe events
Step 3 — CascadingValue for subtree state
Wrap a subtree with <CascadingValue> to flow data to all descendants without passing it through every intermediate component.
@* In a layout or parent component *@
<CascadingValue Value="theme">
@Body
</CascadingValue>
@code {
private ThemeInfo theme = new() { ButtonClass = "btn-primary" };
}
Consume in any descendant:
[CascadingParameter]
private ThemeInfo? Theme { get; set; }
Rules:
- Matched by type, not name. To cascade multiple values of the same type, add
Name:<CascadingValue Value="primary" Name="PrimaryTheme">...</CascadingValue>[CascadingParameter(Name = "PrimaryTheme")] private ThemeInfo? Primary { get; set; } - Set
IsFixed="true"when the value never changes — avoids subscription overhead. - Does NOT cross render mode boundaries. A
<CascadingValue>in a static SSR parent is invisible to interactive children. See Step 6.
Step 4 — CascadingValueSource<T> for app-wide state
Register a CascadingValueSource<T> in DI when the value must be available to all components regardless of render mode.
// Program.cs
builder.Services.AddCascadingValue(sp =>
{
var theme = new ThemeInfo { ButtonClass = "btn-primary" };
return new CascadingValueSource<ThemeInfo>(theme, isFixed: false);
});
Consume identically to Step 3:
[CascadingParameter]
private ThemeInfo? Theme { get; set; }
To update and notify subscribers, either mutate the existing object or replace it:
@* 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() (no argument) also works — mutate the object and then call it. NotifyChangedAsync(newValue) replaces the value and notifies in one step.
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.
Rules:
isFixed: falseenables change notifications.isFixed: trueis better for truly static values (feature flags).- Crosses render mode boundaries — works for per-page interactivity, global interactivity, and WebAssembly. Key advantage over
<CascadingValue>. - Keep cascaded types granular. Every
NotifyChangedAsyncre-renders ALL subscribers regardless of which property changed. Don't put all app state into one cascaded type. - For Auto/WebAssembly apps, register in both server and
.ClientProgram.cs. The type must be in a shared assembly.
Step 5 — Scoped state service with change events
For mutable shared state that multiple components read and write (shopping cart, notification count, filters), use a scoped service with an event for change notification.
Define the service:
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();
}
}
Register as scoped:
builder.Services.AddScoped<CartState>();
Subscribe in components:
@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;
}
}
The 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:
private Action? _handler;
protected override void OnInitialized()
{
_handler = () => InvokeAsync(StateHasChanged);
Cart.OnChange += _handler;
}
public void Dispose() => Cart.OnChange -= _handler;
Store the delegate in a field so you can unsubscribe the exact same instance.
Step 6 — Render mode and service lifetime rules
Cascading values don't cross render mode boundaries
A <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.
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.
Service lifetime on Server vs WebAssembly
| Lifetime | Server | WebAssembly |
|---|---|---|
| Scoped | Per circuit (per user connection) | Per browser tab |
| Singleton | Shared across ALL users | Per browser tab (safe) |
| Transient | New instance per injection | New instance per injection |
On 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>().
On WebAssembly, singletons are per-tab and safe. But code meant for both Server and WebAssembly (Auto mode) must use scoped.
Auto/WebAssembly with prerendering
State 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.
Don'ts
- Don't use a singleton for per-user state on Server — all circuits share it, leaking state between users.
- Don't put all app state into one cascaded object —
NotifyChangedAsyncre-renders ALL subscribers on every change. Separate concerns into distinct types (ThemeState,CartState,UserPreferences). - Don't forget to unsubscribe — omitting
Disposeon event subscriptions causes memory leaks that grow per-circuit. - Don't use
<CascadingValue>in a static layout expecting it to reach interactive children — it won't cross render mode boundaries. Use DI-registeredCascadingValueSource<T>or scoped services. - Don't use
NavigationManager.Refresh(forceReload: true)to propagate cascading value changes — this destroys the circuit and forces a full page reload. Instead, injectCascadingValueSource<T>and callNotifyChangedAsync(newValue)to push updates to all[CascadingParameter]subscribers without a page reload. - Don't call
StateHasChangedfrom a non-Blazor thread — wrap inInvokeAsync. The framework throwsInvalidOperationException: The current thread is not associated with the Dispatcher.
Files in this skill
1coordinate-components skill questions
How do I install the coordinate-components skill in Claude Code?
Run npx degit dotnet/skills/plugins/dotnet-blazor/skills/coordinate-components ~/.claude/skills/coordinate-components. Claude Code loads skills from ~/.claude/skills (all projects) or .claude/skills (one project) and uses a skill when its description matches the task.
Which clients can use the coordinate-components skill?
Any client that reads the Agent Skills format: Claude Code, Codex, Gemini CLI and claude.ai (upload as a ZIP). Scripts in the skill run only where the client allows code execution.
What is the risk of using a singleton service for user state on Blazor Server?
On Blazor Server, singletons are shared across all users, meaning one user's state will leak to others. Use scoped services instead.
How can I notify components when a cascading value changes?
Inject CascadingValueSource<T> and call NotifyChangedAsync() to trigger re-rendering in all subscribers.