Skip to content

Conversation

@ilonatommy
Copy link
Member

@ilonatommy ilonatommy commented Nov 21, 2025

Fixes #64159.

Source of the issue:

_webRootComponentManager is not defined in

return remoteComponentState.ParentComponentState != null ?

How it typically works :

_webRootComponentManager gets created when CircuitHost goes through

var webRootComponentManager = Renderer.GetOrCreateWebRootComponentManager();

That is the normal flow for prerendered/SSR roots: the client sends an UpdateRootComponents message, CircuitHost materialises the operations, the manager is created, and every root component added in that batch is registered inside the manager’s dictionary.

How it works with dynamic JS root:

  • The root cames from JavaScript via Blazor.rootComponents.add(...). That call hits CircuitJSComponentInterop.AddRootComponent, which simply calls base.AddRootComponent(...) on the renderer:
    const componentId = await getRequiredManager().invokeMethodAsync<number>('AddRootComponent', componentIdentifier, containerIdentifier);

    var id = base.AddRootComponent(identifier, domElementSelector);

    (getRequiredManager is not the manager we are interested in, it's only a set of interop methods available for a given component, see
    function getInteropMethods(rendererId: number): DotNet.DotNetObject {
    )
  • This bypasses the whole CircuitHost.PerformRootComponentOperations pipeline, so _webRootComponentManager never gets initialised and no entry is recorded for the new component.
  • When the renderer later constructs RemoteComponentState for that JS-added root, RemoteComponentState.GetComponentKey() invokes RemoteRenderer.GetMarkerKey(...). Because the manager had never been created on this code path, _webRootComponentManager was still null and the null-forgiving access ! resulted in the NullReferenceException.

Fix:

  • Make sure the manager is initialized.

Copilot AI review requested due to automatic review settings November 21, 2025 11:29
@ilonatommy ilonatommy requested a review from a team as a code owner November 21, 2025 11:29
@github-actions github-actions bot added the area-blazor Includes: Blazor, Razor Components label Nov 21, 2025
@ilonatommy ilonatommy changed the title Dynnamic JS roots were entering GetMarkerKey Make sure WebRootComponentManager is initialized for dynamic JS roots Nov 21, 2025
Copilot finished reviewing on behalf of ilonatommy November 21, 2025 11:32
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR fixes a bug where dynamically added JavaScript root components with persistent state were causing circuit errors because they were entering the GetMarkerKey method without the _webRootComponentManager being initialized. The fix ensures the manager is created if it doesn't exist when getting the marker key.

Key changes:

  • Modified GetMarkerKey method in both RemoteRenderer.cs and WebAssemblyRenderer.cs to lazily initialize _webRootComponentManager if null
  • Added test coverage with a new E2E test to verify JS-added persistent state root components work correctly

Reviewed Changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/Components/Server/src/Circuits/RemoteRenderer.cs Fixed GetMarkerKey to call GetOrCreateWebRootComponentManager() when manager is null
src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs Fixed GetMarkerKey to call GetOrCreateWebRootComponentManager() when manager is null
src/Components/test/E2ETest/Tests/StatePersistenceTest.cs Added new test case for JS-added persistent state root components
src/Components/test/testassets/BasicTestApp/wwwroot/persistent-state-js-root-component.html Added test HTML page for JS root component scenario
src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs Registered CounterWithPersistentState component for JavaScript testing
src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PersistentState/CounterWithPersistentState.razor Added test component with persistent state for testing

ilonatommy and others added 3 commits November 21, 2025 12:55
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…onents/Pages/PersistentState/CounterWithPersistentState.razor

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@ilonatommy ilonatommy requested a review from javiercn November 21, 2025 12:16
@ilonatommy ilonatommy marked this pull request as draft November 21, 2025 14:17
@ilonatommy
Copy link
Member Author

ilonatommy commented Nov 24, 2025

The tests:
Circuit_CanShutDownAndReInitializeMultipleTimes, DotNetObjectReference_CannotBeUsed_AfterCircuitShutsDown_AndANewCircuitIsInitialized, StateIsProvidedEveryTimeACircuitGetsCreated fail not because of the framework changes but because of test project changes to RazorComponentEndpointsStartup.cs:

 + options.RootComponents.RegisterForJavaScript<TestContentPackage.PersistentComponents.ComponentWithPersistentState>("dynamic-js-root-counter");

The failure happens on reinitialization after shutdown. The reinitialization process doesn't properly handle the case where JS components with persistent state are registered but not yet added to the circuit. The circuit seems to get "stuck" during reinitialization - it connects (WebSocket shows connected):

[2025-11-24T12:43:08Z] [Info] http://127.0.0.1:51676/subdir/_framework/blazor.web.crlaspqfcs.js 0:59500 "[2025-11-24T12:43:08.420Z] Information: WebSocket connected to ws://127.0.0.1:51676/subdir/_blazor?id=shfXstthdREtTim5LfjW8g."

the HTML shows the component markup, but the components never become interactive (<span id="is-interactive-1">False</span> never changes to True). This suggests the circuit initialization hangs or fails silently somewhere.. (?)

The question is: how adding RegisterForJavaScript (calling enableJSRootComponents on client etc) influences the way we detect interactivity?

@ilonatommy
Copy link
Member Author

ilonatommy commented Nov 25, 2025

The question is: how adding RegisterForJavaScript (calling enableJSRootComponents on client etc) influences the way we detect interactivity?

Calling enableJSRootComponents throws when called more than once which breaks the flow:

throw new Error('Dynamic root components have already been enabled.');

@javiercn, I'm planning to remove that exception and replace it with no-op.

Edit:
the comment above the exception "This will only happen in very nonstandard cases where someone has multiple hosts." makes me think no-op is not a good solution. I will file a separate issue (#64523) and make a workaround for tests to have this PR fix the original problem only.

@ilonatommy ilonatommy marked this pull request as ready for review November 25, 2025 10:53
@ilonatommy ilonatommy self-assigned this Nov 25, 2025
@ilonatommy ilonatommy added this to the .NET 11 Planning milestone Nov 25, 2025
@ilonatommy ilonatommy enabled auto-merge (squash) November 25, 2025 14:54
Copy link
Member

@javiercn javiercn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!

@ilonatommy ilonatommy merged commit c61ff67 into dotnet:main Nov 25, 2025
30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-blazor Includes: Blazor, Razor Components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Blazor] PersistentState attribute causes javascript rendered components to fail.

2 participants