Skip to content

Commit df1dfc4

Browse files
pkozlowski-opensourcedylhunn
authored andcommitted
fix(core): make sure that lifecycle hooks are not tracked (#49701)
Angular lifecycle hooks should never be run as part of the reactive context: we do not expect that signal reads in lifecycle hooks report to any consumers. In the current Angular some of the lifecycle hooks can be flushed early, while executting template update pass. We need to make sure that signal reads in those lifecycle hooks do not register as part of the effect that marks components for check. " PR Close #49701
1 parent a4e749f commit df1dfc4

File tree

10 files changed

+76
-13
lines changed

10 files changed

+76
-13
lines changed

packages/core/src/render3/hooks.ts

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

99
import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, DoCheck, OnChanges, OnDestroy, OnInit} from '../interface/lifecycle_hooks';
10+
import {setActiveConsumer} from '../signals';
1011
import {assertDefined, assertEqual, assertNotEqual} from '../util/assert';
1112

1213
import {assertFirstCreatePass} from './assert';
@@ -238,6 +239,22 @@ function callHooks(
238239
}
239240
}
240241

242+
/**
243+
* Executes a single lifecycle hook, making sure that:
244+
* - it is called in the non-reactive context;
245+
* - profiling data are registered.
246+
*/
247+
function callHookInternal(directive: any, hook: () => void) {
248+
profiler(ProfilerEvent.LifecycleHookStart, directive, hook);
249+
const prevConsumer = setActiveConsumer(null);
250+
try {
251+
hook.call(directive);
252+
} finally {
253+
setActiveConsumer(prevConsumer);
254+
profiler(ProfilerEvent.LifecycleHookEnd, directive, hook);
255+
}
256+
}
257+
241258
/**
242259
* Execute one hook against the current `LView`.
243260
*
@@ -258,19 +275,9 @@ function callHook(currentView: LView, initPhase: InitPhaseState, arr: HookData,
258275
(currentView[PREORDER_HOOK_FLAGS] >> PreOrderHookFlags.NumberOfInitHooksCalledShift) &&
259276
(currentView[FLAGS] & LViewFlags.InitPhaseStateMask) === initPhase) {
260277
currentView[FLAGS] += LViewFlags.IndexWithinInitPhaseIncrementer;
261-
profiler(ProfilerEvent.LifecycleHookStart, directive, hook);
262-
try {
263-
hook.call(directive);
264-
} finally {
265-
profiler(ProfilerEvent.LifecycleHookEnd, directive, hook);
266-
}
278+
callHookInternal(directive, hook);
267279
}
268280
} else {
269-
profiler(ProfilerEvent.LifecycleHookStart, directive, hook);
270-
try {
271-
hook.call(directive);
272-
} finally {
273-
profiler(ProfilerEvent.LifecycleHookEnd, directive, hook);
274-
}
281+
callHookInternal(directive, hook);
275282
}
276283
}

packages/core/test/bundling/animations/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -767,6 +767,9 @@
767767
{
768768
"name": "callHook"
769769
},
770+
{
771+
"name": "callHookInternal"
772+
},
770773
{
771774
"name": "callHooks"
772775
},

packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,9 @@
572572
{
573573
"name": "callHook"
574574
},
575+
{
576+
"name": "callHookInternal"
577+
},
575578
{
576579
"name": "callHooks"
577580
},

packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,9 @@
770770
{
771771
"name": "callHook"
772772
},
773+
{
774+
"name": "callHookInternal"
775+
},
773776
{
774777
"name": "callHooks"
775778
},

packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,9 @@
752752
{
753753
"name": "callHook"
754754
},
755+
{
756+
"name": "callHookInternal"
757+
},
755758
{
756759
"name": "callHooks"
757760
},

packages/core/test/bundling/hello_world/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,9 @@
440440
{
441441
"name": "callHook"
442442
},
443+
{
444+
"name": "callHookInternal"
445+
},
443446
{
444447
"name": "callHooks"
445448
},

packages/core/test/bundling/router/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -950,6 +950,9 @@
950950
{
951951
"name": "callHook"
952952
},
953+
{
954+
"name": "callHookInternal"
955+
},
953956
{
954957
"name": "callHooks"
955958
},

packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,9 @@
521521
{
522522
"name": "callHook"
523523
},
524+
{
525+
"name": "callHookInternal"
526+
},
524527
{
525528
"name": "callHooks"
526529
},

packages/core/test/bundling/todo/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,9 @@
680680
{
681681
"name": "callHook"
682682
},
683+
{
684+
"name": "callHookInternal"
685+
},
683686
{
684687
"name": "callHooks"
685688
},

packages/core/test/render3/reactivity_spec.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {AfterViewInit, Component, destroyPlatform, effect, inject, Injector, NgZone, signal} from '@angular/core';
9+
import {AfterViewInit, Component, destroyPlatform, effect, inject, Injector, Input, NgZone, OnChanges, signal, SimpleChanges} from '@angular/core';
1010
import {TestBed} from '@angular/core/testing';
1111
import {bootstrapApplication} from '@angular/platform-browser';
1212
import {withBody} from '@angular/private/testing';
@@ -216,4 +216,36 @@ describe('effects', () => {
216216

217217
await bootstrapApplication(Cmp);
218218
}));
219+
220+
it('should allow writing to signals in ngOnChanges', () => {
221+
@Component({
222+
selector: 'with-input',
223+
standalone: true,
224+
template: '{{inSignal()}}',
225+
})
226+
class WithInput implements OnChanges {
227+
inSignal = signal<string|undefined>(undefined);
228+
@Input() in : string|undefined;
229+
230+
ngOnChanges(changes: SimpleChanges): void {
231+
if (changes.in) {
232+
this.inSignal.set(changes.in.currentValue);
233+
}
234+
}
235+
}
236+
237+
@Component({
238+
selector: 'test-cmp',
239+
standalone: true,
240+
imports: [WithInput],
241+
template: `<with-input [in]="'A'" />|<with-input [in]="'B'" />
242+
`,
243+
})
244+
class Cmp {
245+
}
246+
247+
const fixture = TestBed.createComponent(Cmp);
248+
fixture.detectChanges();
249+
expect(fixture.nativeElement.textContent).toBe('A|B');
250+
});
219251
});

0 commit comments

Comments
 (0)