Skip to content

Commit

Permalink
test(core): Add test to ensure writing to signals in afterRender hook…
Browse files Browse the repository at this point in the history
…s throws error (angular#52475)

We do not yet handle running change detection again if `afterRender`
hooks write to signals.

PR Close angular#52475
  • Loading branch information
atscott authored and tbondwilkinson committed Dec 6, 2023
1 parent fd39938 commit 8fb8f3e
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 21 deletions.
44 changes: 24 additions & 20 deletions packages/core/src/render3/instructions/change_detection.ts
Expand Up @@ -46,26 +46,7 @@ export function detectChangesInternal<T>(

try {
refreshView(tView, lView, tView.template, context);
let retries = 0;
// If after running change detection, this view still needs to be refreshed or there are
// descendants views that need to be refreshed due to re-dirtying during the change detection
// run, detect changes on the view again. We run change detection in `Targeted` mode to only
// refresh views with the `RefreshView` flag.
while (lView[FLAGS] & (LViewFlags.RefreshView | LViewFlags.HasChildViewsToRefresh) ||
lView[REACTIVE_TEMPLATE_CONSUMER]?.dirty) {
if (retries === MAXIMUM_REFRESH_RERUNS) {
throw new RuntimeError(
RuntimeErrorCode.INFINITE_CHANGE_DETECTION,
ngDevMode &&
'Infinite change detection while trying to refresh views. ' +
'There may be components which each cause the other to require a refresh, ' +
'causing an infinite loop.');
}
retries++;
// Even if this view is detached, we still detect changes in targeted mode because this was
// the root of the change detection run.
detectChangesInView(lView, ChangeDetectionMode.Targeted);
}
detectChangesInViewWhileDirty(lView);
} catch (error) {
if (notifyErrorHandler) {
handleError(lView, error);
Expand All @@ -85,6 +66,29 @@ export function detectChangesInternal<T>(
}
}

function detectChangesInViewWhileDirty(lView: LView) {
let retries = 0;
// If after running change detection, this view still needs to be refreshed or there are
// descendants views that need to be refreshed due to re-dirtying during the change detection
// run, detect changes on the view again. We run change detection in `Targeted` mode to only
// refresh views with the `RefreshView` flag.
while (lView[FLAGS] & (LViewFlags.RefreshView | LViewFlags.HasChildViewsToRefresh) ||
lView[REACTIVE_TEMPLATE_CONSUMER]?.dirty) {
if (retries === MAXIMUM_REFRESH_RERUNS) {
throw new RuntimeError(
RuntimeErrorCode.INFINITE_CHANGE_DETECTION,
ngDevMode &&
'Infinite change detection while trying to refresh views. ' +
'There may be components which each cause the other to require a refresh, ' +
'causing an infinite loop.');
}
retries++;
// Even if this view is detached, we still detect changes in targeted mode because this was
// the root of the change detection run.
detectChangesInView(lView, ChangeDetectionMode.Targeted);
}
}

export function checkNoChangesInternal<T>(
tView: TView, lView: LView, context: T, notifyErrorHandler = true) {
setIsInCheckNoChangesMode(true);
Expand Down
Expand Up @@ -7,7 +7,8 @@
*/

import {NgFor, NgIf} from '@angular/common';
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, Directive, inject, Input, signal, ViewChild} from '@angular/core';
import {PLATFORM_BROWSER_ID} from '@angular/common/src/platform_id';
import {afterNextRender, ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, Directive, inject, Input, PLATFORM_ID, signal, ViewChild} from '@angular/core';
import {TestBed} from '@angular/core/testing';

describe('CheckAlways components', () => {
Expand Down Expand Up @@ -823,6 +824,31 @@ describe('OnPush components with signals', () => {
fixture.componentInstance.cdr.detectChanges();
expect(fixture.nativeElement.innerText).toEqual('2');
});

// Note: We probably don't want this to throw but need to decide how to handle reruns
// This asserts current behavior and should be updated when this is handled
it('throws error when writing to a signal in afterRender', () => {
const counter = signal(0);

@Component({
selector: 'test-component',
standalone: true,
template: ` {{counter()}} `,
})
class TestCmp {
counter = counter;
constructor() {
afterNextRender(() => {
this.counter.set(1);
});
}
}
TestBed.configureTestingModule(
{providers: [{provide: PLATFORM_ID, useValue: PLATFORM_BROWSER_ID}]});

const fixture = TestBed.createComponent(TestCmp);
expect(() => fixture.detectChanges()).toThrowError(/ExpressionChanged/);
});
});


Expand Down

0 comments on commit 8fb8f3e

Please sign in to comment.