Allow JS root components to reinitialize on circuit restart#22
Conversation
WalkthroughThe pull request refactors JS root component initialization to track active renderer IDs and ensure initializers run only once per page load. It consolidates test coverage from a dedicated test class into an existing test fixture and simplifies startup configuration by making JS root component registration unconditional. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/Components/Web.JS/src/Rendering/JSRootComponents.ts`:
- Around line 119-150: The guard in enableJSRootComponents is inverted and the
parameters map isn't stored: change the renderer check to throw when a different
renderer is trying to re-enable (i.e., if manager is set and currentRendererId
!== rendererId) so same-renderer re-enables are accepted, and restore the
parameters by assigning the passed jsComponentParameters to the module-level
jsComponentParametersByIdentifier (or the correct global name used elsewhere)
immediately after setting manager/currentRendererId so
RootComponentsFunctions.add can use it.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
src/Components/Web.JS/src/Rendering/JSRootComponents.tssrc/Components/Web.JS/src/Rendering/WebRendererInteropMethods.tssrc/Components/test/E2ETest/Tests/StatePersistanceJSRootTest.cssrc/Components/test/E2ETest/Tests/StatePersistenceTest.cssrc/Components/test/testassets/Components.TestServer/Program.cssrc/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs
💤 Files with no reviewable changes (2)
- src/Components/test/testassets/Components.TestServer/Program.cs
- src/Components/test/E2ETest/Tests/StatePersistanceJSRootTest.cs
🧰 Additional context used
🧬 Code graph analysis (3)
src/Components/test/E2ETest/Tests/StatePersistenceTest.cs (1)
src/Shared/E2ETesting/WaitAssert.cs (1)
Click(83-87)
src/Components/Web.JS/src/Rendering/WebRendererInteropMethods.ts (1)
src/Components/Web.JS/src/Rendering/JSRootComponents.ts (1)
enableJSRootComponents(120-151)
src/Components/Web.JS/src/Rendering/JSRootComponents.ts (1)
src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts (1)
DotNetObject(697-718)
🔇 Additional comments (4)
src/Components/Web.JS/src/Rendering/JSRootComponents.ts (1)
12-16: State tracking additions look good.src/Components/Web.JS/src/Rendering/WebRendererInteropMethods.ts (1)
32-35: LGTM — updated call signature matches new API.src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs (1)
52-55: Unconditional JS root registration looks fine.src/Components/test/E2ETest/Tests/StatePersistenceTest.cs (1)
279-291: Nice targeted coverage for dynamic JS roots.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| // Called by the framework | ||
| export function enableJSRootComponents( | ||
| rendererId: number, | ||
| managerInstance: DotNet.DotNetObject, | ||
| jsComponentParameters: JSComponentParametersByIdentifier, | ||
| jsComponentInitializers: JSComponentIdentifiersByInitializer | ||
| ): void { | ||
| if (manager) { | ||
| // This will only happen in very nonstandard cases where someone has multiple hosts. | ||
| // It's up to the developer to ensure that only one of them enables dynamic root components. | ||
| if (manager && currentRendererId === rendererId) { | ||
| // A different renderer type (e.g., Server vs WebAssembly) is trying to enable JS root components. | ||
| // This is a multi-host scenario which is not supported for dynamic root components. | ||
| throw new Error('Dynamic root components have already been enabled.'); | ||
| } | ||
|
|
||
| // When the same renderer type re-enables (e.g., circuit restart or new circuit on same page), | ||
| // accept the new manager. The old manager's DotNetObjectReference is no longer valid anyway | ||
| // because the old circuit is gone. We don't dispose the old manager - doing so would cause | ||
| // JSDisconnectedException because the circuit that created it no longer exists. | ||
| currentRendererId = rendererId; | ||
| manager = managerInstance; | ||
| jsComponentParametersByIdentifier = jsComponentParameters; | ||
|
|
||
| // Call the registered initializers. This is an arbitrary subset of the JS component types that are registered | ||
| // on the .NET side - just those of them that require some JS-side initialization (e.g., to register them | ||
| // as custom elements). | ||
| for (const [initializerIdentifier, componentIdentifiers] of Object.entries(jsComponentInitializers)) { | ||
| const initializerFunc = DotNet.findJSFunction(initializerIdentifier, 0) as JSComponentInitializerCallback; | ||
| for (const componentIdentifier of componentIdentifiers) { | ||
| const parameters = jsComponentParameters[componentIdentifier]; | ||
| initializerFunc(componentIdentifier, parameters); | ||
|
|
||
| if (!hasInitializedJsComponents) { | ||
| // Call the registered initializers. This is an arbitrary subset of the JS component types that are registered | ||
| // on the .NET side - just those of them that require some JS-side initialization (e.g., to register them | ||
| // as custom elements). | ||
| for (const [initializerIdentifier, componentIdentifiers] of Object.entries(jsComponentInitializers)) { | ||
| const initializerFunc = DotNet.findJSFunction(initializerIdentifier, 0) as JSComponentInitializerCallback; | ||
| for (const componentIdentifier of componentIdentifiers) | ||
| initializerFunc(componentIdentifier, jsComponentParameters[componentIdentifier]); | ||
| } | ||
|
|
||
| hasInitializedJsComponents = true; | ||
| } |
There was a problem hiding this comment.
Fix renderer guard and restore parameters assignment.
The guard currently throws on the same renderer and allows different renderers; this flips the intended behavior. Also, jsComponentParametersByIdentifier is never assigned, so RootComponentsFunctions.add can throw at runtime.
🐛 Proposed fix
- if (manager && currentRendererId === rendererId) {
+ if (manager && currentRendererId !== rendererId) {
// A different renderer type (e.g., Server vs WebAssembly) is trying to enable JS root components.
// This is a multi-host scenario which is not supported for dynamic root components.
throw new Error('Dynamic root components have already been enabled.');
}
// When the same renderer type re-enables (e.g., circuit restart or new circuit on same page),
// accept the new manager. The old manager's DotNetObjectReference is no longer valid anyway
// because the old circuit is gone. We don't dispose the old manager - doing so would cause
// JSDisconnectedException because the circuit that created it no longer exists.
currentRendererId = rendererId;
manager = managerInstance;
+ jsComponentParametersByIdentifier = jsComponentParameters;
if (!hasInitializedJsComponents) {
// Call the registered initializers. This is an arbitrary subset of the JS component types that are registered
// on the .NET side - just those of them that require some JS-side initialization (e.g., to register them
// as custom elements).
for (const [initializerIdentifier, componentIdentifiers] of Object.entries(jsComponentInitializers)) {
const initializerFunc = DotNet.findJSFunction(initializerIdentifier, 0) as JSComponentInitializerCallback;
for (const componentIdentifier of componentIdentifiers)
initializerFunc(componentIdentifier, jsComponentParameters[componentIdentifier]);
}
hasInitializedJsComponents = true;
}🤖 Prompt for AI Agents
In `@src/Components/Web.JS/src/Rendering/JSRootComponents.ts` around lines 119 -
150, The guard in enableJSRootComponents is inverted and the parameters map
isn't stored: change the renderer check to throw when a different renderer is
trying to re-enable (i.e., if manager is set and currentRendererId !==
rendererId) so same-renderer re-enables are accepted, and restore the parameters
by assigning the passed jsComponentParameters to the module-level
jsComponentParametersByIdentifier (or the correct global name used elsewhere)
immediately after setting manager/currentRendererId so
RootComponentsFunctions.add can use it.
Benchmark PR from qodo-benchmark#90
Summary by CodeRabbit
New Features
Refactor
✏️ Tip: You can customize this high-level summary in your review settings.