Loading...
Loading...
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).
npx skill4agent add dotnet/skills coordinate-componentsAGENTS.md| Need | Mechanism | When to use |
|---|---|---|
| Subtree (same render mode) | | Theme, layout config within a layout |
| App-wide (all render modes) | | Current user, feature flags, theme shared globally |
| Mutable shared state within a circuit | Scoped service + | Shopping cart, notification count, selected filters |
[Parameter]EventCallbackauthor-componentsupport-prerenderingCascadingValueSource<T>Program.csAddCascadingValue(...)isFixed: false[CascadingParameter]NotifyChangedAsync(newValue)StateHasChangedInvokeAsyncIDisposable<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"<CascadingValue>CascadingValueSource<T>// Program.cs
builder.Services.AddCascadingValue(sp =>
{
var theme = new ThemeInfo { ButtonClass = "btn-primary" };
return new CascadingValueSource<ThemeInfo>(theme, isFixed: false);
});[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: falseisFixed: true<CascadingValue>NotifyChangedAsync.ClientProgram.cspublic 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 OnChangeCart.Add(…)InvokeAsyncprivate Action? _handler;
protected override void OnInitialized()
{
_handler = () => InvokeAsync(StateHasChanged);
Cart.OnChange += _handler;
}
public void Dispose() => Cart.OnChange -= _handler;<CascadingValue>MainLayout.razornullCascadingValueSource<T>| 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 |
AddScoped<T>().ClientProgram.cssupport-prerendering[PersistentState]NotifyChangedAsyncThemeStateCartStateUserPreferencesDispose<CascadingValue>CascadingValueSource<T>NavigationManager.Refresh(forceReload: true)CascadingValueSource<T>NotifyChangedAsync(newValue)[CascadingParameter]StateHasChangedInvokeAsyncInvalidOperationException: The current thread is not associated with the Dispatcher