Passing values from the parent component with CascadingParameter
Sharing parameters across multiple components is a common scenario in web applications. It boosts performance as data can be shared rather than being requested from an external source by each component. It also simplifies the code, especially in parent-child scenarios. In Blazor, that’s where the concept of CascadingParameter
comes into play. Its counterpart, CascadingValue
, allows you to provide a value that cascades down the component tree. This pair enables child components to receive and use this shared data or state. This approach solves the challenge of passing information through component hierarchies without complex plumbing or tightly coupled communication.
Let’s implement a Cart
service and pass it downward in a cascading fashion so that we can intercept it within the offer area represented by Ticket
components. We’ll also render the Cart
summary – fully decoupled from the Ticket
behavior.
Getting ready
Before we start exploring how to pass the cascading value, do the following:
- Create a
Recipe06
directory – this will be your working directory - Copy the
Ticket
component from the Ensuring that a parameter is required recipe or copy its implementation from theChapter01
/Recipe05
directory of this book’s GitHub repository.
How to do it...
Follow these steps to implement CascadingParameter
for value sharing:
- Add a
Cart
class and declare supportingContent
andValue
properties. ExtendCart
so that you can communicate state changes by requiring a fallbackAction
property with a primary constructor and implement the basicAdd()
method that triggers this notification:public class Cart(Action onStateHasChanged) { public List<string> Content { get; init; } = []; public decimal Value { get; private set; } public int Volume => Content.Count; public void Add(string tariff, decimal price) { Content.Add(tariff); Value += price; onStateHasChanged.Invoke(); } }
- Create a
SellingTickets
component so that our tickets can be sold:
Figure 1.8: Adding a new SellingTickets component
- Use the
@rendermode
attribute to declare thatSellingTickets
operates inInteractiveWebAssembly
mode and a@page
directive to enable routing:@page "/ch01r06" @rendermode InteractiveWebAssembly
- In the
@code
block ofSellingTickets
, declare theCart
object and initialize it within theOnInitialized()
life cycle method:@code { protected Cart Cart; protected override void OnInitialized() { Cart = new(() => InvokeAsync(StateHasChanged)); } }
- In the
SellingTickets
markup, add theCascadingValue
wrapper with theCart
instance as its value. Declare two sellable tickets within the cart’s operational scope, leveraging theTicket
component:<CascadingValue Value="Cart"> <Ticket Tariff="Adult" Price="10.00m" /> <Ticket Tariff="Child" Price="5.00m" /> </CascadingValue>
- Below the
Cart
area of theSellingTickets
markup, append additional markup to display theCart
summary:<div class="cart-summary"> <div class="cart-content"> Items: @Cart.Volume </div> <div class="cart-value">Price: @Cart.Value</div> </div>
- Navigate to the
Ticket
component. In the@code
block, declareCascadingParameter
so that you can intercept theCart
instance and replace theOnAdded
parameter with anAdd()
method:@code { [CascadingParameter] public Cart Cart { get; set; } public void Add() => Cart.Add(Tariff, Price); }
- In the
Ticket
markup, replace the@onclick
button action so that you can execute the newAdd()
method:<div class="ticket-actions"> <button @onclick="@Add">Add to cart</button> </div>
How it works...
In step 1, we implemented the Cart
class. We declared a Value
property to hold the current cart value and a Content
collection to store added ticket tariffs. We also implemented a parameterless Volume
method to calculate the amount of tickets currently in the cart. Then, we implemented an Add()
method that, in addition to the normal logic for adding to the cart, is responsible for communicating those changes to external objects by invoking the onStateHasChanged
delegate, which is passed using the primary constructor pattern. That way, we ensured Cart
initialization requires us to provide an action to execute upon state changes.
In step 2, we created a SellingTickets
component. In step 3, we declared it to render in InteractiveWebAssembly
mode and leveraged the @page
directive to enable routing. In step 4, in the @code
block of SellingTickets
, we declared a Cart
instance. We initialized Cart
as part of the overridden OnInitialized()
life cycle method and, as an invokable Action
delegate responsible for applying state changes, we passed in the StateHasChanged()
life cycle method. With that in place, any change in the Cart
object will prompt Blazor to recalculate DOM changes at the level of the SellingTicket
component. To avoid any threading or race condition issues, we wrapped the StateHasChanged()
method in the InvokeAsync()
component base method. In step 5, we implemented the SellingTickets
markup. We used a CascadingValue
component and assigned Cart
as its value. We also declared CascadingValue
content by adding two Ticket
instances, representing tickets available for sale. In step 6, we extended the SellingTickets
markup further by adding a section that contained the summary of the cart, showing its current size and value.
In step 7, we navigated to the @code
block of the Ticket
component and declared CascadingParameter
there. Blazor will intercept this parameter’s value as it cascades from a parent component. Notably, we didn’t use EditorRequired
here – as Blazor resolves the cascading value just in time, it would have no impact on compilation. With Cart
available in the scope of the Ticket
component, we replaced the existing OnAdded
parameter with an Add()
method that invokes Cart.Add()
directly. In step 8, we updated the Ticket
markup by replacing the outdated @onclick
assignment on the existing button with a reference to the newly implemented Add()
method.
There’s more...
So, why does the Cart
implementation require an Action
delegate to work? Here, StateHasChanged()
is a component life cycle method, so it triggers DOM re-rendering scoped to that component and its nested children. Since adding to the cart happens at the Ticket
component level and invokes StateHasChanged()
, there’s no impact on the parent SellingTickets
component, and the Cart
summary section remains unchanged! Having the Action
delegate allows the Cart
object to persist a reference to the origin component and thus trigger a DOM update at any level of the component tree.