Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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 US
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 8% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 74% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 40% | 0% |
| case-09 | ✓→✓ | = Same ✓ | 67% | 0% |
| case-16 | ✓→✓ | = Same ✓ | 36% | 0% |
Read AGENTS.md at the workspace root to learn the project's conventions before making changes.
| 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.
CascadingValueSource<T> (Step 4)Program.cs with AddCascadingValue(...) and isFixed: false[CascadingParameter] in child componentsNotifyChangedAsync(newValue) — never page reloadStateHasChanged from background threads in InvokeAsyncIDisposable — dispose timers, cancel tokens, unsubscribe eventsWrap a subtree with <CascadingValue> to flow data to all descendants without passing it through every intermediate component.
razor@* In a layout or parent component *@ <CascadingValue Value="theme"> @Body </CascadingValue> @code { private ThemeInfo theme = new() { ButtonClass = "btn-primary" }; }
Consume in any descendant:
csharp[CascadingParameter] private ThemeInfo? Theme { get; set; }
Rules:
Name:razor <CascadingValue Value="primary" Name="PrimaryTheme">...</CascadingValue> csharp [CascadingParameter(Name = "PrimaryTheme")] private ThemeInfo? Primary { get; set; }
IsFixed="true" when the value never changes — avoids subscription overhead.<CascadingValue> in a static SSR parent is invisible to interactive children. See Step 6.Register a CascadingValueSource<T> in DI when the value must be available to all components regardless of render mode.
csharp// 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:
csharp[CascadingParameter] private ThemeInfo? Theme { get; set; }
To update and notify subscribers, either mutate the existing object or replace it:
razor@* 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: false enables change notifications. isFixed: true is better for truly static values (feature flags).<CascadingValue>.NotifyChangedAsync re-renders ALL subscribers regardless of which property changed. Don't put all app state into one cascaded type..Client Program.cs. The type must be in a shared assembly.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:
csharppublic 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:
csharpbuilder.Services.AddScoped<CartState>();
Subscribe in components:
razor@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:
csharpprivate 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.
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.
| 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.
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.
NotifyChangedAsync re-renders ALL subscribers on every change. Separate concerns into distinct types (ThemeState, CartState, UserPreferences).Dispose on event subscriptions causes memory leaks that grow per-circuit.<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.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.StateHasChanged from a non-Blazor thread — wrap in InvokeAsync. The framework throws InvalidOperationException: The current thread is not associated with the Dispatcher.Other measured skills in the registry, with their headline benchmark lift.