Frontmatter
| id | 8066 |
| title | Standardize Renderer Component Lifecycle Management |
| state | Closed |
| labels | airefactoringarchitecture |
| assignees | tobiu |
| createdAt | Dec 9, 2025, 2:48 AM |
| updatedAt | Dec 9, 2025, 3:54 AM |
| githubUrl | https://github.com/neomjs/neo/issues/8066 |
| author | tobiu |
| commentsCount | 1 |
| parentIssue | null |
| subIssues | [] |
| subIssuesCompleted | 0 |
| subIssuesTotal | 0 |
| blockedBy | [] |
| blocking | [] |
| closedAt | Dec 9, 2025, 3:54 AM |
Standardize Renderer Component Lifecycle Management

Input from Gemini:
✦ I have verified the rationale and implementation of the
mountedstate handling inContentComponentandMarkdownRenderer.Analysis: The regression was caused by the initial
mountedsetter call during construction (wherevalueisfalseandoldValueisundefined), which prematurely triggeredupdateComponentState. Additionally, simply settingcomponent.mounted = truewas insufficient for re-mounting components that were removed from the DOM but kept in memory.Fixes Implemented:
ContentComponent.afterSetMounted: Addedif (me.isConstructed)check. This prevents the renderer update from running during the initial config application, ensuring it only responds to actual lifecycle changes (like tab activation/deactivation).MarkdownRenderer.updateComponentState:
- For
mounted === true: Changedcomponent.mounted = truetocomponent.initVnode(true). This forces a re-initialization of the VNode tree and a fresh mount, ensuring the component correctly re-attaches to the DOM with its current state.- For mounted === false: Kept component.mounted = false. Crucially, this does NOT trigger a DOM unmount itself (the parent container handles the DOM removal). This step simply updates the internal state flag of the child component to reflect that it is no longer in the document, ensuring its state is consistent for the next mount cycle.
Conclusion: The changes correctly address the lifecycle synchronization issue. The use of
initVnode(true)is the correct approach for re-mounting components that have been detached but preserved in memory, especially in the context of the Portal app's card layout where DOM nodes are removed but JS instances persist.
Refactor the management of child components generated by renderers to ensure consistent lifecycle handling (mounting/destroying) across different contexts (LivePreview vs ContentComponent).
Problem:
ContentComponentmanually manages themountedstate of Markdown-embedded components (livePreviews,customComponents) to support re-mounting (e.g., navigating between portal tabs). This logic is tightly coupled to the container and not reusable byLivePreviewwhen running inmarkdownmode.Solution: Shift the responsibility of managing these "floating" components to the renderer that creates them.
Changes:
src/code/renderer/Markdown.mjs:activeComponentsarray to track created instances.updateComponentState(mounted): IteratesactiveComponentsand sets theirmountedconfig.destroyComponents(): Destroys allactiveComponentsand clears the array.render()to populateactiveComponentsinstead of returning them.src/code/renderer/Base.mjs:updateComponentState(mounted)(no-op by default).destroyComponents()(no-op by default).src/code/LivePreview.mjs:customComponentsandlivePreviews.afterSetMounted(value): Callthis.renderer?.updateComponentState(value).destroyChildInstances: Callthis.renderer?.destroyComponents().apps/portal/view/learn/ContentComponent.mjs:customComponentsandlivePreviews.afterSetMounted: Delegate tothis.renderer?.updateComponentState(value).destroyChildInstances: Delegate tothis.renderer?.destroyComponents().Goal: Decouple component lifecycle management from the container view, ensuring
LivePreviewandContentComponentcan both correctly handle re-mounting scenarios for Markdown content.