Skip to content

Commit 70d8511

Browse files
authored
Allow JS root components to reinitialize on circuit restart (#64536)
1 parent 3b674da commit 70d8511

File tree

6 files changed

+39
-59
lines changed

6 files changed

+39
-59
lines changed

src/Components/Web.JS/src/Rendering/JSRootComponents.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ let nextPendingDynamicRootComponentIdentifier = 0;
1010
type ComponentParameters = object | null | undefined;
1111

1212
let manager: DotNet.DotNetObject | undefined;
13+
let currentRendererId: number | undefined;
1314
let jsComponentParametersByIdentifier: JSComponentParametersByIdentifier;
15+
let hasInitializedJsComponents = false;
1416

1517
// These are the public APIs at Blazor.rootComponents.*
1618
export const RootComponentsFunctions = {
@@ -116,28 +118,38 @@ class DynamicRootComponent {
116118

117119
// Called by the framework
118120
export function enableJSRootComponents(
121+
rendererId: number,
119122
managerInstance: DotNet.DotNetObject,
120123
jsComponentParameters: JSComponentParametersByIdentifier,
121124
jsComponentInitializers: JSComponentIdentifiersByInitializer
122125
): void {
123-
if (manager) {
124-
// This will only happen in very nonstandard cases where someone has multiple hosts.
125-
// It's up to the developer to ensure that only one of them enables dynamic root components.
126+
if (manager && currentRendererId !== rendererId) {
127+
// A different renderer type (e.g., Server vs WebAssembly) is trying to enable JS root components.
128+
// This is a multi-host scenario which is not supported for dynamic root components.
126129
throw new Error('Dynamic root components have already been enabled.');
127130
}
128131

132+
// When the same renderer type re-enables (e.g., circuit restart or new circuit on same page),
133+
// accept the new manager. The old manager's DotNetObjectReference is no longer valid anyway
134+
// because the old circuit is gone. We don't dispose the old manager - doing so would cause
135+
// JSDisconnectedException because the circuit that created it no longer exists.
136+
currentRendererId = rendererId;
129137
manager = managerInstance;
130138
jsComponentParametersByIdentifier = jsComponentParameters;
131139

132-
// Call the registered initializers. This is an arbitrary subset of the JS component types that are registered
133-
// on the .NET side - just those of them that require some JS-side initialization (e.g., to register them
134-
// as custom elements).
135-
for (const [initializerIdentifier, componentIdentifiers] of Object.entries(jsComponentInitializers)) {
136-
const initializerFunc = DotNet.findJSFunction(initializerIdentifier, 0) as JSComponentInitializerCallback;
137-
for (const componentIdentifier of componentIdentifiers) {
138-
const parameters = jsComponentParameters[componentIdentifier];
139-
initializerFunc(componentIdentifier, parameters);
140+
if (!hasInitializedJsComponents) {
141+
// Call the registered initializers. This is an arbitrary subset of the JS component types that are registered
142+
// on the .NET side - just those of them that require some JS-side initialization (e.g., to register them
143+
// as custom elements).
144+
for (const [initializerIdentifier, componentIdentifiers] of Object.entries(jsComponentInitializers)) {
145+
const initializerFunc = DotNet.findJSFunction(initializerIdentifier, 0) as JSComponentInitializerCallback;
146+
for (const componentIdentifier of componentIdentifiers) {
147+
const parameters = jsComponentParameters[componentIdentifier];
148+
initializerFunc(componentIdentifier, parameters);
149+
}
140150
}
151+
152+
hasInitializedJsComponents = true;
141153
}
142154
}
143155

src/Components/Web.JS/src/Rendering/WebRendererInteropMethods.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export function attachWebRendererInterop(
3131

3232
if (jsComponentParameters && jsComponentInitializers && Object.keys(jsComponentParameters).length > 0) {
3333
const manager = getInteropMethods(rendererId);
34-
enableJSRootComponents(manager, jsComponentParameters, jsComponentInitializers);
34+
enableJSRootComponents(rendererId, manager, jsComponentParameters, jsComponentInitializers);
3535
}
3636

3737
rendererByIdResolverMap.get(rendererId)?.[0]?.();

src/Components/test/E2ETest/Tests/StatePersistanceJSRootTest.cs

Lines changed: 0 additions & 42 deletions
This file was deleted.

src/Components/test/E2ETest/Tests/StatePersistenceTest.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,20 @@ public async Task StateIsProvidedEveryTimeACircuitGetsCreated(string streaming)
276276
RenderComponentsWithPersistentStateAndValidate(suppressEnhancedNavigation: false, mode, typeof(InteractiveServerRenderMode), streaming, stateValue: "other");
277277
}
278278

279+
[Theory]
280+
[InlineData("ServerNonPrerendered")]
281+
[InlineData("WebAssemblyNonPrerendered")]
282+
public void PersistentStateIsSupportedInDynamicJSRoots(string renderMode)
283+
{
284+
Navigate($"subdir/WasmMinimal/dynamic-js-root.html?renderMode={renderMode}");
285+
286+
Browser.Equal("Counter", () => Browser.Exists(By.TagName("h1")).Text);
287+
Browser.Equal("Current count: 0", () => Browser.Exists(By.CssSelector("p[role='status']")).Text);
288+
289+
Browser.Click(By.CssSelector("button.btn-primary"));
290+
Browser.Equal("Current count: 1", () => Browser.Exists(By.CssSelector("p[role='status']")).Text);
291+
}
292+
279293
private void BlockWebAssemblyResourceLoad()
280294
{
281295
// Clear local storage so that the resource hash is not found

src/Components/test/testassets/Components.TestServer/Program.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ public static async Task Main(string[] args)
2424
["CORS (WASM)"] = (BuildWebHost<CorsStartup>(CreateAdditionalArgs(args)), "/subdir"),
2525
["Prerendering (Server-side)"] = (BuildWebHost<PrerenderedStartup>(CreateAdditionalArgs(args)), "/prerendered"),
2626
["Razor Component Endpoints"] = (BuildWebHost<RazorComponentEndpointsStartup<App>>(CreateAdditionalArgs(args)), "/subdir"),
27-
["Razor Component Endpoints with JS Root Component"] = (BuildWebHost<RazorComponentEndpointsStartup<App>>(CreateAdditionalArgs([.. args, "--RegisterDynamicJSRootComponent", "true"])), "/subdir"),
2827
["Deferred component content (Server-side)"] = (BuildWebHost<DeferredComponentContentStartup>(CreateAdditionalArgs(args)), "/deferred-component-content"),
2928
["Locked navigation (Server-side)"] = (BuildWebHost<LockedNavigationStartup>(CreateAdditionalArgs(args)), "/locked-navigation"),
3029
["Client-side with fallback"] = (BuildWebHost<StartupWithMapFallbackToClientSideBlazor>(CreateAdditionalArgs(args)), "/fallback"),

src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,7 @@ public void ConfigureServices(IServiceCollection services)
5151
options.DisconnectedCircuitMaxRetained = 0;
5252
options.DetailedErrors = true;
5353
}
54-
if (Configuration.GetValue<bool>("RegisterDynamicJSRootComponent"))
55-
{
56-
options.RootComponents.RegisterForJavaScript<TestContentPackage.PersistentComponents.ComponentWithPersistentState>("dynamic-js-root-counter");
57-
}
54+
options.RootComponents.RegisterForJavaScript<TestContentPackage.PersistentComponents.ComponentWithPersistentState>("dynamic-js-root-counter");
5855
})
5956
.AddAuthenticationStateSerialization(options =>
6057
{

0 commit comments

Comments
 (0)