Events

Typed events and reactive state for components anywhere in the tree

Purpose of Component

RForge.Blazor.Events lets components talk to each other from anywhere in the tree, without a shared parent, without parameters passed down through every layer, and without a hand written service full of C# events. Derive from RfEventComponent and it takes care of subscribing, re-rendering on the right thread, error handling and disposal for you.

It has two parts. Events (IEvent) are fire and forget messages that any component or service can publish and any component can handle. State (IState<T>) is a store-like value with a current value and change notification. Writing an equal value is a no-op, so record equality keeps components from re-rendering needlessly.

When to Use

When two components that are far apart in the tree need to share a value or react to the same thing: a cart badge that updates when a product card adds an item, a theme picker that restyles unrelated components, a toast host that any component can ask for a message.

Features

  • Selectors: re-render only when the part of the state you use changes.
  • Scopes: EventScope and StateScope isolate events and give subtrees their own state. Routes include Bubble (the default), Local, Broadcast, Root, Global and Named.
  • Source generator: [Subscribe] and [UseState] need no reflection, so they are trim and AOT safe. Analyzers (RFEVT001 to RFEVT014) catch mistakes at build time.
  • Every render mode: static SSR, Interactive Server, WebAssembly and Auto, with or without prerendering, and opt-in state persistence from prerender to interactive.
  • Plain components too: opt-in cascading values let third party components read state through [CascadingParameter].

Basic Usage

Define the events and state as records, publish and write from any component, and react anywhere else in the tree. The tutorials walk through each part step by step.

Define Events and State

public sealed record ItemAdded(int ProductId, int Quantity) : IEvent;

public sealed record CartState(int Count, decimal Total) : IState<CartState>
{
    public static CartState Initial => new(0, 0m);
}

Publish and Write State

// ProductCard.razor
@inherits RfEventComponent

<button @onclick="AddToCart">Add</button>
// ProductCard.razor.cs
public partial class ProductCard
{
    [Parameter] public Product Product { get; set; } = default!;

    private async Task AddToCart()
    {
        State<CartState>().Update(s => s with { Count = s.Count + 1, Total = s.Total + Product.Price });
        await PublishAsync(new ItemAdded(Product.Id, 1));
    }
}

React Anywhere Else

CartBadge needs no shared parent with ProductCard and no parameters. The selector makes it re-render only when the count changes.

// CartBadge.razor
@inherits RfEventComponent

<span class="tag @(_pulse ? "is-warning" : "")">@Count</span>
// CartBadge.razor.cs
public partial class CartBadge
{
    [UseState(Select = nameof(SelectCount))]
    private partial int Count { get; }

    private static int SelectCount(CartState s) => s.Count;

    [Subscribe]
    private void OnItemAdded(ItemAdded e) => _pulse = true;

    private bool _pulse;
}

Scope a Subtree

Wrap a subtree in EventScope and StateScope so each instance gets its own event channel and its own copy of the state.

<EventScope Name="checkout">
    <StateScope TState="CheckoutForm">
        <AddressForm />
        <PaymentForm />
    </StateScope>
    <OrderSummary />
</EventScope>

Render Mode Support

Mode Support
Static SSR Full within the request: one hub per request. Nothing updates after the HTML is sent.
Interactive Server Full. One hub per circuit (per user).
Interactive WebAssembly Full. One hub per browser app instance.
Interactive Auto Full, within whichever runtime the component is running in.
Prerendering Prerendered HTML reflects state loaded during the render. Opt-in PersistState(...) carries root state into the interactive session.
Server and WebAssembly islands on one page Not supported. Separate containers mean separate hubs.

Diagnostics

The analyzer that ships with the package reports these at build time. Errors stop the build; warnings and info point at likely bugs. Troubleshooting explains each fix.

ID Severity Meaning
RFEVT001 Error Attribute used in a .razor @code block. Move the member to the .razor.cs code-behind.
RFEVT002 Error The type, or a type containing it, is not partial.
RFEVT003 Error The type does not derive from RfEventComponent. Add @inherits RfEventComponent.
RFEVT004 Error Invalid [Subscribe] signature.
RFEVT005 Error The handler's first parameter is not an IEvent.
RFEVT006 Error A [UseState] property is not a partial get-only declaration.
RFEVT007 Error A [UseState] property type is not a state type. Use IState<T> or add Select.
RFEVT008 Error Invalid selector.
RFEVT009 Error Attributes and a RegisterSubscriptions override in the same component.
RFEVT010 Warning Inconsistent Scope and ScopeName.
RFEVT011 Warning Two handlers for the same event in one component.
RFEVT012 Info The event type is not immutable. Use a record with init-only properties.
RFEVT013 Warning The state type is not a record.
RFEVT014 Warning The component implements IDisposable or IAsyncDisposable itself. Override DisposeAsyncCore instead.

How to Setup

  1. Install the package
  2. Register the event hub
  3. Make a component event-aware

Install the Package

Install RForge.Blazor.Events for the events alone. The RForge.Blazor package installs the whole family (components, events and the auto API generator) instead.

Command Line
dotnet add package RForge.Blazor.Events
Package Manager Console
Install-Package RForge.Blazor.Events

One package contains the runtime, the source generator and the analyzers. Add it to every project that contains event-aware components. In a Blazor Web App that is both the server project and the .Client project, because NuGet does not pass analyzers through project references.

Register the Event Hub

Call AddRForgeEvents in every Program.cs that hosts interactive components, including the .Client project for WebAssembly and Auto. Each runtime has its own container, so each needs its own hub.

builder.Services.AddRForgeEvents();

Make a Component Event-aware

Start the .razor file with @inherits RfEventComponent and put [Subscribe] and [UseState] members in the code-behind (.razor.cs) partial class, never in an @code block. Source generators cannot see code that the Razor compiler produces, and RFEVT001 reports it if you forget.

Requirements

Requirement Details
.NET .NET 8, 9 or 10. Events, state, scopes and the analyzers work with the .NET 8 SDK and newer.
C# 13 for [UseState] [UseState] is a partial property, which needs C# 13. net9 and net10 projects already compile at C# 13 or later. A net8 project defaults to C# 12, so it needs the .NET 9 SDK or newer and <LangVersion>13</LangVersion> (or latest).
Async lambdas at C# 12 In a net8 project at C# 12, builder.On<MyEvent>(async e => ...) is ambiguous between the Task and ValueTask overloads. Pass a method group, cast the lambda to Func<MyEvent, Task>, or set LangVersion to 13 or later.
Server and WebAssembly islands Islands in different runtimes use different containers, so they never share a hub. See the Render Modes example.

Which Package Do I Install?

You want Install
The UI components only RForge.Blazor.Components
Typed events and state only RForge.Blazor.Events
An API generated from a service interface RForge.Blazor.AutoApi.Abstractions and RForge.Blazor.AutoApi.Generator
Everything RForge.Blazor
An unhandled error has occurred. Reload 🗙