|
1 | 1 | import type { ReactElement } from 'react'; |
2 | | -import type { RegisteredComponent, RailsContext } from './types/index.ts'; |
| 2 | +import type { RegisteredComponent, RailsContext, RenderReturnType } from './types/index.ts'; |
3 | 3 | import ComponentRegistry from './ComponentRegistry.ts'; |
4 | 4 | import StoreRegistry from './StoreRegistry.ts'; |
5 | 5 | import createReactOutput from './createReactOutput.ts'; |
6 | 6 | import reactHydrateOrRender from './reactHydrateOrRender.ts'; |
7 | 7 | import { getRailsContext } from './context.ts'; |
8 | 8 | import { isServerRenderHash } from './isServerRenderResult.ts'; |
| 9 | +import { onPageUnloaded } from './pageLifecycle.ts'; |
| 10 | +import { supportsRootApi, unmountComponentAtNode } from './reactApis.cts'; |
9 | 11 |
|
10 | 12 | const REACT_ON_RAILS_STORE_ATTRIBUTE = 'data-js-react-on-rails-store'; |
11 | 13 |
|
| 14 | +// Track all rendered roots for cleanup |
| 15 | +const renderedRoots = new Map<string, { root: RenderReturnType; domNode: Element }>(); |
| 16 | + |
12 | 17 | function initializeStore(el: Element, railsContext: RailsContext): void { |
13 | 18 | const name = el.getAttribute(REACT_ON_RAILS_STORE_ATTRIBUTE) || ''; |
14 | 19 | const props = el.textContent !== null ? (JSON.parse(el.textContent) as Record<string, unknown>) : {}; |
@@ -95,7 +100,9 @@ function renderElement(el: Element, railsContext: RailsContext): void { |
95 | 100 | You returned a server side type of react-router error: ${JSON.stringify(reactElementOrRouterResult)} |
96 | 101 | You should return a React.Component always for the client side entry point.`); |
97 | 102 | } else { |
98 | | - reactHydrateOrRender(domNode, reactElementOrRouterResult as ReactElement, shouldHydrate); |
| 103 | + const root = reactHydrateOrRender(domNode, reactElementOrRouterResult as ReactElement, shouldHydrate); |
| 104 | + // Track the root for cleanup |
| 105 | + renderedRoots.set(domNodeId, { root, domNode }); |
99 | 106 | } |
100 | 107 | } |
101 | 108 | } catch (e: unknown) { |
@@ -162,3 +169,27 @@ export function reactOnRailsComponentLoaded(domId: string): Promise<void> { |
162 | 169 | renderComponent(domId); |
163 | 170 | return Promise.resolve(); |
164 | 171 | } |
| 172 | + |
| 173 | +/** |
| 174 | + * Unmount all rendered React components and clear roots. |
| 175 | + * This should be called on page unload to prevent memory leaks. |
| 176 | + */ |
| 177 | +function unmountAllComponents(): void { |
| 178 | + renderedRoots.forEach(({ root, domNode }) => { |
| 179 | + try { |
| 180 | + if (supportsRootApi && root && typeof root === 'object' && 'unmount' in root) { |
| 181 | + // React 18+ Root API |
| 182 | + root.unmount(); |
| 183 | + } else { |
| 184 | + // React 16-17 legacy API |
| 185 | + unmountComponentAtNode(domNode); |
| 186 | + } |
| 187 | + } catch (error) { |
| 188 | + console.error('Error unmounting component:', error); |
| 189 | + } |
| 190 | + }); |
| 191 | + renderedRoots.clear(); |
| 192 | +} |
| 193 | + |
| 194 | +// Register cleanup on page unload |
| 195 | +onPageUnloaded(unmountAllComponents); |
0 commit comments