New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Template of an OnPush component is no longer updated after a signal change in some cases #50320
Comments
This is an interesting case, thnx for sharing! What is (roughly, working theory) going on here is that a computed property is marked as "stale" (by the update in ngOnChanges) after the reactive context of the OnPush component accessed this value (and marked it as "clean"). This means that subsequent signal change notification do not reach the computed. Ultimately the update in ngOnChanges is the "culprit" here but we do need better guidance / guide-rails here. Trying to narrow down the reproduction to a simpler case. |
Here is a simpler repro: https://stackblitz.com/edit/angular-hu21mk?file=src%2Fmain.ts This is |
I have the same bug here with ExpressionChanged error when changing signal value in ngOnDestroy. Will give stackblitz soon, maybe it will help |
@pkozlowski-opensource Thank you for your answers. I don't see I find updating signals in ngOnChanges or ngOnDestroy quite useful. I am a bit afraid when you say:
Do you mean that you are planning to only add a clear error and better documentation to "fix" this bug? Ideally, it should be possible to update signals at any time (without having to add an asynchronous step), with no error, with the new values correctly taken into account inside templates, and without any resurgence of the old |
Yes, I encountered a similar issue and I described it in an RFC as well. |
Do you have any news about this bug? |
I'm still trying to wrap my head about the details of what is going on here and my working theory is the same as previously: modifying signals (during CD) that is also marking a view as dirty puts in trouble. Essentially we are marking a signals as dirty and it is trying to mark a view as dirty, but the very same view is being change detected. Here is a simplified reproduce scenario, still working on making it smaller: https://stackblitz.com/edit/stackblitz-starters-t5x8cv?file=src%2Fmain.ts |
Here is a more up to date reproduction using Angular v17 next and some |
Another simplification of the reproduction scenario, this time with the latest Angular version (17.x next) https://stackblitz.com/edit/vdjh5r-pe334b?file=package.json,angular.json,src%2Findex.html,src%2Fmain.ts |
@divdavem please have a look at the latest, trimmed down reproduction (which I think corresponds to the initial use-case). What we need to observe there is that the situation involves writing to a signal that is read by a template being change detected. Currently the Angular doesn't have any mechanism for "going back" to the already change detected views and it is logically equivalent to the We are going to make this situation an explicit error in v17. Having said this we more than understand that is is hard for a developer to know what is going on with the change detection - it requires deep awareness of the fwk internals and this is this not great. This is why we are working on the updated change detection algorithm that will handle the situation described here without any errors and proper UI refresh. |
Issue angular#50320 shows that in some cases, updating a signal that's a dependency of a template during change detection of that template can have several adverse effects. This can happen, for example, if the signal is set during the lifecycle hook of a directive within the same template that reads the signal. This can cause a few things to happen: * Straightforwardly, it can cause `ExpressionChanged` errors. * Surprisingly, it can cause an assertion within the `ReactiveLViewConsumer` to fail. * Very surprisingly, it can cause change detection for an `OnPush` component to stop working. The root cause of these later behaviors is subtle, and is ultimately a desync between the reactive graph and the view tree's notion of "dirty" for a given view. This will be fixed with further work planned for change detection to handle such updates directly. Until then, this commit improves the DX through two changes: 1. The mechanism of "committing" `ReactiveLViewConsumer`s to a view is changed to use the `consumerOnSignalRead` hook from the reactive graph. This prevents the situation which required the assertion in the first place. 2. A `console.warn` warning is added when a view is marked dirty via a signal while it's still executing. The warning informs users that they're pushing data against the direction of change detection, risking `ExpressionChanged` or other issues. It's a warning and not an error because the check is overly broad and captures situations where the application would not actually break as a result, such as if a `computed` marked the template dirty but still returned the same value.
Issue angular#50320 shows that in some cases, updating a signal that's a dependency of a template during change detection of that template can have several adverse effects. This can happen, for example, if the signal is set during the lifecycle hook of a directive within the same template that reads the signal. This can cause a few things to happen: * Straightforwardly, it can cause `ExpressionChanged` errors. * Surprisingly, it can cause an assertion within the `ReactiveLViewConsumer` to fail. * Very surprisingly, it can cause change detection for an `OnPush` component to stop working. The root cause of these later behaviors is subtle, and is ultimately a desync between the reactive graph and the view tree's notion of "dirty" for a given view. This will be fixed with further work planned for change detection to handle such updates directly. Until then, this commit improves the DX through two changes: 1. The mechanism of "committing" `ReactiveLViewConsumer`s to a view is changed to use the `consumerOnSignalRead` hook from the reactive graph. This prevents the situation which required the assertion in the first place. 2. A `console.warn` warning is added when a view is marked dirty via a signal while it's still executing. The warning informs users that they're pushing data against the direction of change detection, risking `ExpressionChanged` or other issues. It's a warning and not an error because the check is overly broad and captures situations where the application would not actually break as a result, such as if a `computed` marked the template dirty but still returned the same value.
Issue angular#50320 shows that in some cases, updating a signal that's a dependency of a template during change detection of that template can have several adverse effects. This can happen, for example, if the signal is set during the lifecycle hook of a directive within the same template that reads the signal. This can cause a few things to happen: * Straightforwardly, it can cause `ExpressionChanged` errors. * Surprisingly, it can cause an assertion within the `ReactiveLViewConsumer` to fail. * Very surprisingly, it can cause change detection for an `OnPush` component to stop working. The root cause of these later behaviors is subtle, and is ultimately a desync between the reactive graph and the view tree's notion of "dirty" for a given view. This will be fixed with further work planned for change detection to handle such updates directly. Until then, this commit improves the DX through two changes: 1. The mechanism of "committing" `ReactiveLViewConsumer`s to a view is changed to use the `consumerOnSignalRead` hook from the reactive graph. This prevents the situation which required the assertion in the first place. 2. A `console.warn` warning is added when a view is marked dirty via a signal while it's still executing. The warning informs users that they're pushing data against the direction of change detection, risking `ExpressionChanged` or other issues. It's a warning and not an error because the check is overly broad and captures situations where the application would not actually break as a result, such as if a `computed` marked the template dirty but still returned the same value.
Issue angular#50320 shows that in some cases, updating a signal that's a dependency of a template during change detection of that template can have several adverse effects. This can happen, for example, if the signal is set during the lifecycle hook of a directive within the same template that reads the signal. This can cause a few things to happen: * Straightforwardly, it can cause `ExpressionChanged` errors. * Surprisingly, it can cause an assertion within the `ReactiveLViewConsumer` to fail. * Very surprisingly, it can cause change detection for an `OnPush` component to stop working. The root cause of these later behaviors is subtle, and is ultimately a desync between the reactive graph and the view tree's notion of "dirty" for a given view. This will be fixed with further work planned for change detection to handle such updates directly. Until then, this commit improves the DX through two changes: 1. The mechanism of "committing" `ReactiveLViewConsumer`s to a view is changed to use the `consumerOnSignalRead` hook from the reactive graph. This prevents the situation which required the assertion in the first place. 2. A `console.warn` warning is added when a view is marked dirty via a signal while it's still executing. The warning informs users that they're pushing data against the direction of change detection, risking `ExpressionChanged` or other issues. It's a warning and not an error because the check is overly broad and captures situations where the application would not actually break as a result, such as if a `computed` marked the template dirty but still returned the same value.
Issue angular#50320 shows that in some cases, updating a signal that's a dependency of a template during change detection of that template can have several adverse effects. This can happen, for example, if the signal is set during the lifecycle hook of a directive within the same template that reads the signal. This can cause a few things to happen: * Straightforwardly, it can cause `ExpressionChanged` errors. * Surprisingly, it can cause an assertion within the `ReactiveLViewConsumer` to fail. * Very surprisingly, it can cause change detection for an `OnPush` component to stop working. The root cause of these later behaviors is subtle, and is ultimately a desync between the reactive graph and the view tree's notion of "dirty" for a given view. This will be fixed with further work planned for change detection to handle such updates directly. Until then, this commit improves the DX through two changes: 1. The mechanism of "committing" `ReactiveLViewConsumer`s to a view is changed to use the `consumerOnSignalRead` hook from the reactive graph. This prevents the situation which required the assertion in the first place. 2. A `console.warn` warning is added when a view is marked dirty via a signal while it's still executing. The warning informs users that they're pushing data against the direction of change detection, risking `ExpressionChanged` or other issues. It's a warning and not an error because the check is overly broad and captures situations where the application would not actually break as a result, such as if a `computed` marked the template dirty but still returned the same value.
Issue #50320 shows that in some cases, updating a signal that's a dependency of a template during change detection of that template can have several adverse effects. This can happen, for example, if the signal is set during the lifecycle hook of a directive within the same template that reads the signal. This can cause a few things to happen: * Straightforwardly, it can cause `ExpressionChanged` errors. * Surprisingly, it can cause an assertion within the `ReactiveLViewConsumer` to fail. * Very surprisingly, it can cause change detection for an `OnPush` component to stop working. The root cause of these later behaviors is subtle, and is ultimately a desync between the reactive graph and the view tree's notion of "dirty" for a given view. This will be fixed with further work planned for change detection to handle such updates directly. Until then, this commit improves the DX through two changes: 1. The mechanism of "committing" `ReactiveLViewConsumer`s to a view is changed to use the `consumerOnSignalRead` hook from the reactive graph. This prevents the situation which required the assertion in the first place. 2. A `console.warn` warning is added when a view is marked dirty via a signal while it's still executing. The warning informs users that they're pushing data against the direction of change detection, risking `ExpressionChanged` or other issues. It's a warning and not an error because the check is overly broad and captures situations where the application would not actually break as a result, such as if a `computed` marked the template dirty but still returned the same value. PR Close #52234
Issue angular#50320 shows that in some cases, updating a signal that's a dependency of a template during change detection of that template can have several adverse effects. This can happen, for example, if the signal is set during the lifecycle hook of a directive within the same template that reads the signal. This can cause a few things to happen: * Straightforwardly, it can cause `ExpressionChanged` errors. * Surprisingly, it can cause an assertion within the `ReactiveLViewConsumer` to fail. * Very surprisingly, it can cause change detection for an `OnPush` component to stop working. The root cause of these later behaviors is subtle, and is ultimately a desync between the reactive graph and the view tree's notion of "dirty" for a given view. This will be fixed with further work planned for change detection to handle such updates directly. Until then, this commit improves the DX through two changes: 1. The mechanism of "committing" `ReactiveLViewConsumer`s to a view is changed to use the `consumerOnSignalRead` hook from the reactive graph. This prevents the situation which required the assertion in the first place. 2. A `console.warn` warning is added when a view is marked dirty via a signal while it's still executing. The warning informs users that they're pushing data against the direction of change detection, risking `ExpressionChanged` or other issues. It's a warning and not an error because the check is overly broad and captures situations where the application would not actually break as a result, such as if a `computed` marked the template dirty but still returned the same value.
Issue #50320 shows that in some cases, updating a signal that's a dependency of a template during change detection of that template can have several adverse effects. This can happen, for example, if the signal is set during the lifecycle hook of a directive within the same template that reads the signal. This can cause a few things to happen: * Straightforwardly, it can cause `ExpressionChanged` errors. * Surprisingly, it can cause an assertion within the `ReactiveLViewConsumer` to fail. * Very surprisingly, it can cause change detection for an `OnPush` component to stop working. The root cause of these later behaviors is subtle, and is ultimately a desync between the reactive graph and the view tree's notion of "dirty" for a given view. This will be fixed with further work planned for change detection to handle such updates directly. Until then, this commit improves the DX through two changes: 1. The mechanism of "committing" `ReactiveLViewConsumer`s to a view is changed to use the `consumerOnSignalRead` hook from the reactive graph. This prevents the situation which required the assertion in the first place. 2. A `console.warn` warning is added when a view is marked dirty via a signal while it's still executing. The warning informs users that they're pushing data against the direction of change detection, risking `ExpressionChanged` or other issues. It's a warning and not an error because the check is overly broad and captures situations where the application would not actually break as a result, such as if a `computed` marked the template dirty but still returned the same value. PR Close #52427
…tion The significance of the combination of angular#51854 and angular#52302 went mostly unnoticed. The first removed a unidirectional data flow constraint for transplanted views and the second updated the signal implementation to share transplanted view logic. The result is that we automatically get behavior that (mostly) removes `ExpressionChangedAfterItWasCheckedError` when signals are used to drive application state to DOM synchronization. fixes angular#50320
…tion The significance of the combination of angular#51854 and angular#52302 went mostly unnoticed. The first removed a unidirectional data flow constraint for transplanted views and the second updated the signal implementation to share transplanted view logic. The result is that we automatically get behavior that (mostly) removes `ExpressionChangedAfterItWasCheckedError` when signals are used to drive application state to DOM synchronization. fixes angular#50320
…tion The significance of the combination of angular#51854 and angular#52302 went mostly unnoticed. The first removed a unidirectional data flow constraint for transplanted views and the second updated the signal implementation to share transplanted view logic. The result is that we automatically get behavior that (mostly) removes `ExpressionChangedAfterItWasCheckedError` when signals are used to drive application state to DOM synchronization. fixes angular#50320
…tion The significance of the combination of angular#51854 and angular#52302 went mostly unnoticed. The first removed a unidirectional data flow constraint for transplanted views and the second updated the signal implementation to share transplanted view logic. The result is that we automatically get behavior that (mostly) removes `ExpressionChangedAfterItWasCheckedError` when signals are used to drive application state to DOM synchronization. fixes angular#50320
…tion The significance of the combination of angular#51854 and angular#52302 went mostly unnoticed. The first removed a unidirectional data flow constraint for transplanted views and the second updated the signal implementation to share transplanted view logic. The result is that we automatically get behavior that (mostly) removes `ExpressionChangedAfterItWasCheckedError` when signals are used to drive application state to DOM synchronization. fixes angular#50320
…tion (#52476) The significance of the combination of #51854 and #52302 went mostly unnoticed. The first removed a unidirectional data flow constraint for transplanted views and the second updated the signal implementation to share transplanted view logic. The result is that we automatically get behavior that (mostly) removes `ExpressionChangedAfterItWasCheckedError` when signals are used to drive application state to DOM synchronization. fixes #50320 PR Close #52476
…tion (angular#52476) The significance of the combination of angular#51854 and angular#52302 went mostly unnoticed. The first removed a unidirectional data flow constraint for transplanted views and the second updated the signal implementation to share transplanted view logic. The result is that we automatically get behavior that (mostly) removes `ExpressionChangedAfterItWasCheckedError` when signals are used to drive application state to DOM synchronization. fixes angular#50320 PR Close angular#52476
Now that angular/angular#50320 is fixed, it should be possible to change any angular signal at any time.
Now that angular/angular#50320 is fixed, it should be possible to change any angular signal at any time, and there be no need anymore to delay the call of directives.
Now that angular/angular#50320 is fixed, it should be possible to change any angular signal at any time, and there should be no need anymore to delay the call of directives.
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
…tion (angular#52476) The significance of the combination of angular#51854 and angular#52302 went mostly unnoticed. The first removed a unidirectional data flow constraint for transplanted views and the second updated the signal implementation to share transplanted view logic. The result is that we automatically get behavior that (mostly) removes `ExpressionChangedAfterItWasCheckedError` when signals are used to drive application state to DOM synchronization. fixes angular#50320 PR Close angular#52476
…tion (angular#52476) The significance of the combination of angular#51854 and angular#52302 went mostly unnoticed. The first removed a unidirectional data flow constraint for transplanted views and the second updated the signal implementation to share transplanted view logic. The result is that we automatically get behavior that (mostly) removes `ExpressionChangedAfterItWasCheckedError` when signals are used to drive application state to DOM synchronization. fixes angular#50320 PR Close angular#52476
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
appearsClick again on the
Toggle state
button.item is visible
disappearsClick 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:
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
Please provide the environment you discovered this bug in (run
ng version
)Anything else?
What triggers the bug seems to be the fact that the signal is synchronously changed either from
ngOnChanges
or fromngOnDestroy
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:
state
is a direct reference tothis.model.state
inMyComponent
instead of being acomputed
this.state()
is added in the constructor ofMyComponent
MyComponent
is notOnPush
The text was updated successfully, but these errors were encountered: