|
| 1 | +## Effects |
| 2 | + |
| 3 | +Signals are useful because they notify interested consumers when they change. An **effect** is an operation that runs whenever one or more signal values change. You can create an effect with the `effect` function: |
| 4 | + |
| 5 | +```ts |
| 6 | +effect(() => { |
| 7 | + console.log(`The current count is: ${count()}`); |
| 8 | +}); |
| 9 | +``` |
| 10 | + |
| 11 | +Effects always run **at least once.** When an effect runs, it tracks any signal value reads. Whenever any of these signal values change, the effect runs again. Similar to computed signals, effects keep track of their dependencies dynamically, and only track signals which were read in the most recent execution. |
| 12 | + |
| 13 | +Effects always execute **asynchronously**, during the change detection process. |
| 14 | + |
| 15 | +### Use cases for effects |
| 16 | + |
| 17 | +Effects should be the last API you reach for. Always prefer `computed()` for derived values and `linkedSignal()` for values that can be both derived and manually set. If you find yourself copying data from one signal to another with an effect, it's a sign you should move your source-of-truth higher up and use `computed()` or `linkedSignal()` instead. Effects are best for syncing signal state to imperative, non-signal APIs. |
| 18 | + |
| 19 | +TIP: There are no situations where effect is good, only situations where it is appropriate. |
| 20 | + |
| 21 | +- Logging signal values, either for analytics or as a debugging tool. |
| 22 | +- Keeping data in sync with different kind of storges `window.localStorage`, session storage, cookies etc. |
| 23 | +- Adding custom DOM behavior that can't be expressed with template syntax. |
| 24 | +- Performing custom rendering to a `<canvas>` element, charting library, or other third party UI library. |
| 25 | + |
| 26 | +<docs-callout critical title="When not to use effects"> |
| 27 | +Avoid using effects for propagation of state changes. This can result in `ExpressionChangedAfterItHasBeenChecked` errors, infinite circular updates, or unnecessary change detection cycles. |
| 28 | + |
| 29 | +Instead, use `computed` signals to model state that depends on other state. |
| 30 | +</docs-callout> |
| 31 | + |
| 32 | +### Injection context |
| 33 | + |
| 34 | +By default, you can only create an `effect()` within an [injection context](guide/di/dependency-injection-context) (where you have access to the `inject` function). The easiest way to satisfy this requirement is to call `effect` within a component, directive, or service `constructor`: |
| 35 | + |
| 36 | +```ts |
| 37 | +@Component({/* ... */}) |
| 38 | +export class EffectiveCounter { |
| 39 | + readonly count = signal(0); |
| 40 | + |
| 41 | + constructor() { |
| 42 | + // Register a new effect. |
| 43 | + effect(() => { |
| 44 | + console.log(`The count is: ${this.count()}`); |
| 45 | + }); |
| 46 | + } |
| 47 | +} |
| 48 | +``` |
| 49 | + |
| 50 | +To create an effect outside the constructor, you can pass an `Injector` to `effect` via its options: |
| 51 | + |
| 52 | +```ts |
| 53 | +@Component({...}) |
| 54 | +export class EffectiveCounterComponent { |
| 55 | + readonly count = signal(0); |
| 56 | + private injector = inject(Injector); |
| 57 | + |
| 58 | + initializeLogging(): void { |
| 59 | + effect(() => { |
| 60 | + console.log(`The count is: ${this.count()}`); |
| 61 | + }, {injector: this.injector}); |
| 62 | + } |
| 63 | +} |
| 64 | +``` |
| 65 | + |
| 66 | +### Execution of effects |
| 67 | + |
| 68 | +Angular implicitly defines two implicit behaviors for its effects depending on the context they were created in. |
| 69 | + |
| 70 | +A "View Effect" is an `effect` created in the context of a component instantiation. This includes effects created by services that are tied to component injectors.<br> |
| 71 | +A "Root Effect" is created in a root provided service. |
| 72 | + |
| 73 | +The execution of both kind of `effect` are tied to the change detection process. |
| 74 | + |
| 75 | +- "View effects" are executed _before_ there corresponding compoent is checked the change detection process. |
| 76 | +- "Root effects" are executed prior to all components being checked by the change detection process. |
| 77 | + |
| 78 | +In both cases, if at least one of the effect dependency changed during the effect execution, the effect will re-run before moving ahead on the change detection process, |
| 79 | + |
| 80 | +### Destroying effects |
| 81 | + |
| 82 | +When a component or directive is destroyed, Angular automatically cleans up any associated effects. |
| 83 | + |
| 84 | +An `effect` can be created two different context that will affect when it's destroyed: |
| 85 | + |
| 86 | +- A "View effect" is destroyed when the component is destroyed. |
| 87 | +- A "Root effect" is destroyed when the application is destroyed. |
| 88 | + |
| 89 | +Effects return an `EffectRef`. You can use the ref's `destroy` method to manually dispose of an effect. You can combine this with the `manualCleanup` option when creating an effect to disable automatic cleanup. Be careful to actually destroy such effects when they're no longer required. |
| 90 | + |
| 91 | +### Effect cleanup functions |
| 92 | + |
| 93 | +When a component or directive is destroyed, Angular automatically cleans up any associated effects. |
| 94 | +Effects might start long-running operations, which you should cancel if the effect is destroyed or runs again before the first operation finished. When you create an effect, your function can optionally accept an `onCleanup` function as its first parameter. This `onCleanup` function lets you register a callback that is invoked before the next run of the effect begins, or when the effect is destroyed. |
| 95 | + |
| 96 | +```ts |
| 97 | +effect((onCleanup) => { |
| 98 | + const user = currentUser(); |
| 99 | + |
| 100 | + const timer = setTimeout(() => { |
| 101 | + console.log(`1 second ago, the user became ${user}`); |
| 102 | + }, 1000); |
| 103 | + |
| 104 | + onCleanup(() => { |
| 105 | + clearTimeout(timer); |
| 106 | + }); |
| 107 | +}); |
| 108 | +``` |
| 109 | + |
| 110 | +## Side effects on DOM elements |
| 111 | + |
| 112 | +The `effect` function is a general-purpose tool for running code in reaction to signal changes. However, it runs _before_ the Angular updates the DOM. In some situations, you may need to manually inspect or modify the DOM, or integrate a 3rd-party library that requires direct DOM access. |
| 113 | + |
| 114 | +For these situations, you can use `afterRenderEffect`. It functions like `effect`, but runs after Angular has finished rendering and committed its changes to the DOM. |
| 115 | + |
| 116 | +```ts |
| 117 | +@Component({/* ... */}) |
| 118 | +export class MyFancyChart { |
| 119 | + chartData = input.required<ChartData>(); |
| 120 | + canvas = viewChild.required<ElementRef<HTMLCanvasElement>>('canvas'); |
| 121 | + chart: ChartInstance; |
| 122 | + |
| 123 | + constructor() { |
| 124 | + // Run a single time to create the chart instance |
| 125 | + afterNextRender({ |
| 126 | + write: () => { |
| 127 | + this.chart = initializeChart(this.nativeElement(), this.charData()); |
| 128 | + } |
| 129 | + }); |
| 130 | + |
| 131 | + // Re-run after DOM has been updated whenever `chartData` changes |
| 132 | + afterRenderEffect(() => { |
| 133 | + this.chart.updateData(this.chartData()); |
| 134 | + }); |
| 135 | + } |
| 136 | +} |
| 137 | +``` |
| 138 | + |
| 139 | +In this example `afterRenderEffect` to update a chart created by a 3rd party library. |
| 140 | + |
| 141 | +TIP: You often don't need `afterRenderEffect` to check for DOM changes. APIs like `ResizeObserver`, `MutationObserver` and `IntersectionObserver` are preferred to `effect` or afterRenderEffect when possible. |
| 142 | + |
| 143 | +### Render phases |
| 144 | + |
| 145 | +Accessing the DOM and mutating it can impact the performance of your application, for example by triggering to many unecesary [reflows](https://developer.mozilla.org/en-US/docs/Glossary/Reflow). |
| 146 | + |
| 147 | +To optimize those operations, `afterRenderEffect` offers four phases to group the callbacks and execute them in an optimized order. |
| 148 | + |
| 149 | +The phases are: |
| 150 | + |
| 151 | +| Phase | Description | |
| 152 | +| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |
| 153 | +| `earlyRead` | Use this phase to read from the DOM before a subsequent write callback, for example to perform custom layout that the browser doesn't natively support. Prefer the read phase if reading can wait. | |
| 154 | +| `write` | Use this phase to write to the DOM. **Never** read from the DOM in this phase. | |
| 155 | +| `mixedReadWrite` | Use this phase to read from and write to the DOM simultaneously. Never use this phase if it is possible to divide the work among the other phases instead. | |
| 156 | +| `read` | Use this phase to read from the DOM. **Never** write to the DOM in this phase. | |
| 157 | + |
| 158 | +Using these phases helps prevent layout thrashing and ensures that your DOM operations are performed in a safe and efficient manner. |
| 159 | + |
| 160 | +You can specify the phase by passing an object with a `phase` property to `afterRender` or `afterNextRender`: |
| 161 | + |
| 162 | +```ts |
| 163 | +afterRenderEffect({ |
| 164 | + earlyRead: (cleanupFn) => { /* ... */ }, |
| 165 | + write: (previousPhaseValue, cleanupFn) => { /* ... */ }, |
| 166 | + mixedReadWrite: (previousPhaseValue, cleanupFn) => { /* ... */ }, |
| 167 | + read: (previousPhaseValue, cleanupFn) => { /* ... */ }, |
| 168 | +}); |
| 169 | +``` |
| 170 | + |
| 171 | +CRITICAL: If you don't specify the phase, `afterRenderEffect` runs callbacks during the `mixedReadWrite` phase. This may worsen application performance by causing additional DOM reflows. |
| 172 | + |
| 173 | +#### Phase executions |
| 174 | + |
| 175 | +The `earlyRead` phase callback receives no parameters. Each subsequent phase receives the return value of the previous phase's callback as a Signal. You can use this to coordinate work across phases. |
| 176 | + |
| 177 | +Effects run in the following phase order: |
| 178 | + |
| 179 | +1. `earlyRead` |
| 180 | +2. `write` |
| 181 | +3. `mixedReadWrite` |
| 182 | +4. `read` |
| 183 | + |
| 184 | +If one of the phases modifies a signal value tracked by `afterRenderEffect`, the affected phases execute again. |
| 185 | + |
| 186 | +#### Cleanup |
| 187 | + |
| 188 | +Each phase provides a cleanup callback function as argument. The cleanup callbacks are executed when the `afterRenderEffect` is destroyed or before re-running phase effects. |
| 189 | + |
| 190 | +### Server-side rendering caveats |
| 191 | + |
| 192 | +`afterRenderEffect`, similarly to `afterNextRender`/`afterEveryRender`, only runs on the client. |
| 193 | + |
| 194 | +NOTE: Components are not guaranteed to be [hydrated](/guide/hydration) before the callback runs. You must use caution when directly reading or writing the DOM and layout. |
0 commit comments