09 · Render modes

What works where (Static, Server, WebAssembly, Auto), and the island boundary

Goal: know what the hub does in each render mode, and avoid the one boundary it can't cross.

See it running: the Render modes sample page (/events/example/render-modes).

The one rule

A hub belongs to a DI container. Components share a hub only when they share a container.

Where the component runsIts hubShared with
Static SSROne per HTTP requestComponents rendered in that request
Interactive ServerOne per circuitEvery Interactive Server component on the page, for that user
Interactive WebAssemblyOne per browser app instanceEvery WebAssembly component on the page
Interactive AutoServer's or WebAssembly's, depending on which the instance landed onThat runtime's components
PrerenderingOne per request (thrown away afterward)The prerender only. See tutorial 08

What works in each mode

ModeEventsStateNotes
Static SSRwithin the requestwithin the requestNothing updates after the HTML is sent
Interactive ServerWorksWorksPublishing from background threads is safe
Interactive WebAssemblyWorksWorksGenerated code is trim/AOT-safe
Interactive Autoper runtimeper runtimeState doesn't move from Server to WebAssembly

1. Register in both hosts

If any component runs in WebAssembly (WebAssembly or Auto render mode), the client needs its own hub:

// MyApp.Client/Program.cs (excerpt)
builder.Services.AddRForgeEvents(EventHubSetup.Configure);
// MyApp/Program.cs (excerpt)
builder.Services.AddRForgeEvents(EventHubSetup.Configure);

Same EventHubSetup.Configure in both, so CascadeRootState, PersistState and error handling match (tutorial 01).

2. Mixed islands don't talk to each other

On a page with both a Server island and a WebAssembly island, each talks to a different hub:

@* MyApp/Components/Pages/IslandsPage.razor *@
@page "/islands"
@using MyApp.Client.Features.Welcome.Components

@* Static page hosting two islands. *@
<Greeting @rendermode="InteractiveServer" />          @* circuit hub *@
<NameEditor @rendermode="InteractiveServer" />        @* same circuit hub: shares with the Greeting above *@
<NameEditor @rendermode="InteractiveWebAssembly" />   @* WebAssembly hub: typing here does NOT update the Greeting *@

This is a deliberate limitation (ADR-007). Bridging would require JavaScript interop or a server round trip.

What to do instead:

  • Use one interactive render mode per area that has to communicate. Most apps already do: a global render mode on <Routes>, or one per page.
  • If two different-mode islands really must coordinate, use an app-level channel (an API, a SignalR hub, browser storage), and translate it into hub events on each side.

3. Declare scopes inside the island

Cascading values don't cross from static rendering into an interactive island, and scopes are cascading values:

@* MyApp/Components/Pages/ScopeBoundaryPage.razor *@
@page "/scope-boundary"
@using MyApp.Client.Features.Settings.Components

@* ❌ This EventScope is rendered statically. The island inside can't see it, so
      SettingsPanel's children end up attached to the circuit's root hub instead. *@
<EventScope Name="outer">
    <SettingsPanel @rendermode="InteractiveServer" />
</EventScope>

@* SettingsPanel declares its own <EventScope Name="settings"> inside itself,
      so the scope lives in the island. *@
<SettingsPanel @rendermode="InteractiveServer" />

Rule of thumb: a component that needs a scope declares it itself (as in tutorial 05), so it doesn't matter where the component is placed. If your whole app is interactive (render mode set on <Routes>), scopes in layouts work too.

4. Auto: same component, two possible hubs

An InteractiveAuto page runs on Server until the WebAssembly runtime is downloaded, then on WebAssembly for later visits. Each visit gets a fresh hub in whichever runtime it lands on:

  • Root state isn't carried from the Server phase to the WebAssembly phase. Reload data from its source (see "load if not loaded" in tutorial 08).
  • Server-only services (MyApp/Services) aren't available in WebAssembly. Pages that need them should be InteractiveServer.

Best practices

  • Choose one interactive render mode per page region, ideally per page or app-wide.
  • Keep event-aware code in the Client project so it can run anywhere. Put only server-only pages and services in the server project.
  • Let features declare their own scopes, so render-mode boundaries never cut a feature in half.
  • Turn off prerendering (prerender: false) where a page starts background work or the prerendered HTML isn't useful.

Common mistakes

SymptomCauseFix
Two parts of a page don't update each otherThey're different render modes (different hubs)Use the same render mode
No registered service of type 'IRfEventHub' in the browserHub not registered in MyApp.Client/Program.csRegister it (tutorial 01)
A scope "doesn't work" in an islandThe scope was declared in static markup outside the islandDeclare it inside the interactive component
State resets when Auto switches to WebAssemblyNew runtime, new hubReload the data, or keep it in your backend

Next: 10 · Manual registration

An unhandled error has occurred. Reload 🗙