Skip to content

Commit f893d07

Browse files
crisbetokirjs
authored andcommitted
fix(core): destroy renderer when replacing styles during HMR (#59514)
Currently when we swap out the component during HMR, we remove the renderer from the cache, but we never destroy it which means that its styles are still in the DOM. This can cause the old styles to leak into the component after they're replaced. These changes add a `destroy` call to ensure that they're removed. PR Close #59514
1 parent bb35df4 commit f893d07

File tree

2 files changed

+24
-15
lines changed

2 files changed

+24
-15
lines changed

packages/core/src/render3/hmr.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {Type} from '../interface/type';
10-
import {assertDefined} from '../util/assert';
10+
import {assertDefined, assertNotEqual} from '../util/assert';
1111
import {assertLView} from './assert';
1212
import {getComponentDef} from './def_getters';
1313
import {assertComponentDef} from './errors';
@@ -82,23 +82,23 @@ export function ɵɵreplaceMetadata(
8282

8383
/**
8484
* Finds all LViews matching a specific component definition and recreates them.
85-
* @param def Component definition to search for.
85+
* @param oldDef Component definition to search for.
8686
* @param rootLView View from which to start the search.
8787
*/
88-
function recreateMatchingLViews(def: ComponentDef<unknown>, rootLView: LView): void {
88+
function recreateMatchingLViews(oldDef: ComponentDef<unknown>, rootLView: LView): void {
8989
ngDevMode &&
9090
assertDefined(
91-
def.tView,
91+
oldDef.tView,
9292
'Expected a component definition that has been instantiated at least once',
9393
);
9494

9595
const tView = rootLView[TVIEW];
9696

9797
// Use `tView` to match the LView since `instanceof` can
9898
// produce false positives when using inheritance.
99-
if (tView === def.tView) {
100-
ngDevMode && assertComponentDef(def.type);
101-
recreateLView(getComponentDef(def.type)!, rootLView);
99+
if (tView === oldDef.tView) {
100+
ngDevMode && assertComponentDef(oldDef.type);
101+
recreateLView(getComponentDef(oldDef.type)!, oldDef, rootLView);
102102
return;
103103
}
104104

@@ -107,10 +107,10 @@ function recreateMatchingLViews(def: ComponentDef<unknown>, rootLView: LView): v
107107

108108
if (isLContainer(current)) {
109109
for (let i = CONTAINER_HEADER_OFFSET; i < current.length; i++) {
110-
recreateMatchingLViews(def, current[i]);
110+
recreateMatchingLViews(oldDef, current[i]);
111111
}
112112
} else if (isLView(current)) {
113-
recreateMatchingLViews(def, current);
113+
recreateMatchingLViews(oldDef, current);
114114
}
115115
}
116116
}
@@ -131,10 +131,15 @@ function clearRendererCache(factory: RendererFactory, def: ComponentDef<unknown>
131131

132132
/**
133133
* Recreates an LView in-place from a new component definition.
134-
* @param def Definition from which to recreate the view.
134+
* @param newDef Definition from which to recreate the view.
135+
* @param oldDef Previous component definition being swapped out.
135136
* @param lView View to be recreated.
136137
*/
137-
function recreateLView(def: ComponentDef<unknown>, lView: LView<unknown>): void {
138+
function recreateLView(
139+
newDef: ComponentDef<unknown>,
140+
oldDef: ComponentDef<unknown>,
141+
lView: LView<unknown>,
142+
): void {
138143
const instance = lView[CONTEXT];
139144
const host = lView[HOST]!;
140145
// In theory the parent can also be an LContainer, but it appears like that's
@@ -143,25 +148,26 @@ function recreateLView(def: ComponentDef<unknown>, lView: LView<unknown>): void
143148
ngDevMode && assertLView(parentLView);
144149
const tNode = lView[T_HOST] as TElementNode;
145150
ngDevMode && assertTNodeType(tNode, TNodeType.Element);
151+
ngDevMode && assertNotEqual(newDef, oldDef, 'Expected different component definition');
146152

147153
// Recreate the TView since the template might've changed.
148-
const newTView = getOrCreateComponentTView(def);
154+
const newTView = getOrCreateComponentTView(newDef);
149155

150156
// Always force the creation of a new renderer to ensure state captured during construction
151157
// stays consistent with the new component definition by clearing any old cached factories.
152158
const rendererFactory = lView[ENVIRONMENT].rendererFactory;
153-
clearRendererCache(rendererFactory, def);
159+
clearRendererCache(rendererFactory, oldDef);
154160

155161
// Create a new LView from the new TView, but reusing the existing TNode and DOM node.
156162
const newLView = createLView(
157163
parentLView,
158164
newTView,
159165
instance,
160-
getInitialLViewFlagsFromDef(def),
166+
getInitialLViewFlagsFromDef(newDef),
161167
host,
162168
tNode,
163169
null,
164-
rendererFactory.createRenderer(host, def),
170+
rendererFactory.createRenderer(host, newDef),
165171
null,
166172
null,
167173
null,

packages/platform-browser/src/dom/dom_renderer.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,9 @@ export class DomRendererFactory2 implements RendererFactory2, OnDestroy {
190190
* @param componentId ID of the component that is being replaced.
191191
*/
192192
protected componentReplaced(componentId: string) {
193+
// Destroy the renderer so the styles get removed from the DOM, otherwise
194+
// they may leak back into the component together with the new ones.
195+
this.rendererByCompId.get(componentId)?.destroy();
193196
this.rendererByCompId.delete(componentId);
194197
}
195198
}

0 commit comments

Comments
 (0)