fix(core): effects wait for ngOnInit for their first run (#52473)

When an effect is created in a component constructor, it might read signals
which are derived from component inputs. These signals may be unreliable or
(in the case of the proposed input signals) may throw if accessed before the
component is first change detected (which is what makes required inputs
available).

Depending on the scenario involved, the effect may or may not run before
this initialization takes place, which isn't a great developer experience.
In particular, effects created during CD (e.g. via control flow) work fine,
as do effects created in bootstrap thanks to the sync CD it performs. When
an effect is created through dynamic component creation outside of CD though
(such as on router navigations), it runs before the component is first CD'd,
causing the issue.

In fact, in the signal components RFC we described how effects would wait
until ngOnInit for their first execution for exactly this reason, but this
behavior was never implemented as it was thought our effect scheduling
design made it unnecessary. This is true of the regular execution of effects
but the above scenario shows that *creation* of the effect is still
vulnerable. Thus, this logic is needed.

This commit makes effects sensitive to their creation context, by injecting
`ChangeDetectorRef` optionally. An effect created with an injector that's
tied to a component will wait until that component is initialized before
initially being scheduled. TestBed effect flushing is also adjusted to
account for the additional interaction with change detection.

PR Close #52473
This commit is contained in:
Alex Rickabaugh
2023-10-31 12:34:04 -07:00
parent de802f0c1a
commit ee9605f3c8
7 changed files with 232 additions and 25 deletions
@@ -135,6 +135,9 @@ export class ComponentFixture<T> {
// Running without zone. Just do the change detection.
this._tick(checkNoChanges);
}
// Run any effects that were created/dirtied during change detection. Such effects might become
// dirty in response to input signals changing.
this.effectRunner?.flush();
}
/**