13 · Best practices

One-page checklist: project layout, file placement, naming, design rules

A one-page checklist of the recommendations from the tutorials. Each item links to the tutorial that explains it.

Project layout

MyApp/                                  server project
├── Program.cs                          AddRForgeEvents(EventHubSetup.Configure)
├── Services/                           server-only Scoped services that publish/subscribe
└── Components/Pages/                   pages that need server-only services (InteractiveServer)
MyApp.Client/                           anything that may run in WebAssembly
├── Program.cs                          AddRForgeEvents(EventHubSetup.Configure)
├── EventHubSetup.cs                         the single hub configuration
├── AppJsonContext.cs                   JSON metadata for persisted state
├── GlobalUsings.cs                     global using RForge.Blazor.Events;
├── _Imports.razor                      @using RForge.Blazor.Events
├── Features/<Feature>/
│   ├── <Feature>State.cs
│   ├── <Feature>Events.cs
│   └── Components/X.razor + X.razor.cs
└── Pages/
MyApp.Tests/                            bUnit tests, one class per feature

Where each file goes

FilePut it inWhyTutorial
IState recordsMyApp.Client/Features/<Feature>/<Feature>State.csWebAssembly needs them, and the server references the client02
IEvent recordsMyApp.Client/Features/<Feature>/<Feature>Events.csSame, and they sit next to the state they relate to04
Event-aware componentsMyApp.Client/Features/<Feature>/Components/Run in every render mode, and stay next to their state01
Component attributes ([Subscribe], [UseState])The .razor.cs code-behindThe generator can't see @code blocks01
@inherits RfEventComponentThe .razor fileOtherwise Razor declares : ComponentBase (CS0263)01
EventHubSetup.ConfigureMyApp.Client/EventHubSetup.csServer and WebAssembly hubs must be configured identically01
JsonSerializerContextMyApp.Client/AppJsonContext.csOne place listing every persisted state08
Services using databases/secretsMyApp/Services/Must not ship to the browser07
EventScope/StateScopeThe feature's root componentWorks wherever the feature is placed, including inside islands05, 06
Event testsMyApp.Tests/<Feature>Tests.csOne test class per feature mirrors the source layout11

Naming

ThingConventionExample
State<Noun>StateCartState, ThemeState
Event: a factPast tenseItemAdded, SectionSaved
Event: a command<Verb>RequestedToastRequested, SaveAllRequested
SelectorSelect<What>SelectCount, SelectIsEven
HandlerOn<Event>OnItemAdded
Scope nameFeature name, lowercase"checkout", "settings"

State

  • [ ] sealed record implementing IState<T> with a sensible Initial. (02)
  • [ ] Immutable: replace with with. ImmutableList/ImmutableDictionary for collections. (02)
  • [ ] One state per concept; no giant "AppState". (02)
  • [ ] Domain rules live on the record (cart.Add(product)), not in components. (02)
  • [ ] Readers use [UseState] (with a selector for one or two fields). Writers use State<T>(). (02)
  • [ ] Persist only expensive-to-load root state, always "load if not loaded", and [JsonIgnore] derived properties. (08)

Events

  • [ ] sealed record implementing IEvent, carrying ids and small values only. (04)
  • [ ] Use an event for "it happened" and state for "what it is now". Often you'll do both. (04)
  • [ ] Handlers are fast: fire-and-forget long work with DisposalToken. (04)
  • [ ] App-wide listeners (toasts, logging) use ScopeKind.Root. (04, 05)
  • [ ] Don't use the hub for direct parent → child. Use [Parameter]/EventCallback. (04)

Scopes

  • [ ] Commands go down (Route.Named("x", Broadcast)), facts go up (default Bubble). (05)
  • [ ] Target scopes by name rather than by position. (05)
  • [ ] Multiple widget instances each get a StateScope, with starting data passed through Initial. (06)
  • [ ] Child components are written as if the state were global. (06)

Services and threads

  • [ ] Services that use IRfEventHub are Scoped, never Singleton. (07)
  • [ ] Every hub.Subscribe(…) / store.Subscribe(…) in a service is disposed. (07)
  • [ ] Background work stops when its page or circuit goes away, and timers catch ObjectDisposedException. (07)
  • [ ] Components never call InvokeAsync for hub updates. The base class does. (07)

Render modes

  • [ ] AddRForgeEvents(EventHubSetup.Configure) in both Program.cs files. (01)
  • [ ] One interactive render mode per area that must communicate. (09)
  • [ ] Scopes are declared inside the interactive component. (09)
  • [ ] prerender: false for pages that start background work. (07)

Advanced

  • [ ] Prefer attributes. Use manual registration only for what they can't express, and comment why. (10)
  • [ ] With attributes, extend via partial void OnRegisterSubscriptions, never a second override. (10)
  • [ ] Components whose captured parameters can change get a @key. (10)

Testing

  • [ ] A shared EventTestContext that adds the hub and sets RendererInfo. (11)
  • [ ] Arrange through Hub.State<T>(), act inside cut.InvokeAsync, and use spies for published events. (11)
An unhandled error has occurred. Reload 🗙