-
Notifications
You must be signed in to change notification settings - Fork 26.6k
Description
Which @angular/* package(s) are the source of the bug?
core
Is this a regression?
No
Description
Go to this stackblitz
Click on the Toggle state
button.
item is visible
appears
Click again on the Toggle state
button.
item is visible
disappears
Click again on the Toggle state
button.
Nothing happens! I would expect the item is visible
element to appear again.
Here is the code of the stackblitz:
import 'zone.js/dist/zone';
import {
ChangeDetectionStrategy,
Component,
Directive,
effect,
signal,
Input,
OnChanges,
OnDestroy,
SimpleChanges,
computed,
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { bootstrapApplication } from '@angular/platform-browser';
interface State {
isInDom: boolean;
shouldBeInDom: boolean;
}
const createModel = () => {
const state = signal<State>({
isInDom: false,
shouldBeInDom: false,
});
const updateState = (changes: Partial<State>) =>
state.update((value) => ({
...value,
...changes,
}));
const toggle = () => updateState({ shouldBeInDom: !state().shouldBeInDom });
const setDomState = (isInDom: boolean) => updateState({ isInDom });
return { state, toggle, setDomState };
};
@Directive({
standalone: true,
selector: '[myDirective]',
})
export class MyDirective implements OnChanges, OnDestroy {
@Input({ required: true })
myDirective!: ReturnType<typeof createModel>;
async ngOnChanges(changes: SimpleChanges) {
// Comment the following line to have the issue on display:
await 0;
this.myDirective.setDomState(true);
}
async ngOnDestroy() {
// Uncomment the following line as a work-around to fix the issue:
// await 0;
this.myDirective.setDomState(false);
}
}
@Component({
selector: 'my-component',
standalone: true,
// Comment the following line as a work-around to fix the issue:
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, MyDirective],
template: `
<div *ngIf="state().shouldBeInDom" [myDirective]="model">item is visible</div>
`,
})
export class MyComponent {
model = createModel();
// Uncomment the following line as a work-around to fix the issue:
// state = this.model.state;
state = computed(() => this.model.state());
constructor() {
// Uncomment the following line as a work-around to fix the issue:
// effect(() => console.log(this.state()));
}
}
@Component({
selector: 'my-app',
standalone: true,
imports: [CommonModule, MyComponent],
template: `
<button (click)="c.model.toggle()">Toggle state</button>
<my-component #c></my-component>
`,
})
export class App {}
bootstrapApplication(App);
Please provide a link to a minimal reproduction of the bug
https://stackblitz.com/edit/angular-nltu6v?file=src/main.ts
Please provide the exception or error you saw
There is no exception, no explanation.
Please provide the environment you discovered this bug in (run ng version
)
Angular CLI: 16.0.1
Node: 18.16.0
Package Manager: npm 9.5.1
OS: linux x64
Angular: 16.0.1
... animations, cli, common, compiler, compiler-cli, core, forms
... platform-browser, platform-browser-dynamic, router
Package Version
---------------------------------------------------------
@angular-devkit/architect 0.1600.1
@angular-devkit/build-angular 16.0.1
@angular-devkit/core 16.0.1
@angular-devkit/schematics 16.0.1
@schematics/angular 16.0.1
ng-packagr 16.0.1
rxjs 7.8.1
typescript 5.0.4
Anything else?
What triggers the bug seems to be the fact that the signal is synchronously changed either from ngOnChanges
or from ngOnDestroy
in the directive. If this is done asynchronously, the problem does not happen.
This makes me think about the famous ExpressionChangedAfterItHasBeenCheckedError
issue (even if there is no exception displayed in this case), the idea is the same: there is an issue when updating data at some specific times.
As mentioned in the comments of the code, if any of the following modifications is done (alone), the bug does not happen:
- if
state
is a direct reference tothis.model.state
inMyComponent
instead of being acomputed
- if an effect that reads
this.state()
is added in the constructor ofMyComponent
- if
MyComponent
is notOnPush
Metadata
Metadata
Assignees
Labels
Type
Projects
Status