Skip to content

Commit 827f5a6

Browse files
JeanMechealxhub
authored andcommitted
docs: create a separate effect guide
This also includes details about `afterRenderEffect` WIP. fixes angular#63757
1 parent 6a3f6be commit 827f5a6

4 files changed

Lines changed: 208 additions & 97 deletions

File tree

adev/src/app/routing/sub-navigation-data.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
110110
path: 'guide/signals/resource',
111111
contentPath: 'guide/signals/resource',
112112
},
113+
{
114+
label: 'Side effects for non-reactives APIs',
115+
path: 'guide/signals/effect',
116+
contentPath: 'guide/signals/effect',
117+
},
113118
],
114119
},
115120
{
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
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.

adev/src/content/guide/signals/overview.md

Lines changed: 8 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -89,90 +89,20 @@ If you set `showCount` to `true` and then read `conditionalCount` again, the der
8989

9090
Note that dependencies can be removed during a derivation as well as added. If you later set `showCount` back to `false`, then `count` will no longer be considered a dependency of `conditionalCount`.
9191

92-
## Reading signals in `OnPush` components
93-
94-
When you read a signal within an `OnPush` component's template, Angular tracks the signal as a dependency of that component. When the value of that signal changes, Angular automatically [marks](api/core/ChangeDetectorRef#markforcheck) the component to ensure it gets updated the next time change detection runs. Refer to the [Skipping component subtrees](best-practices/skipping-subtrees) guide for more information about `OnPush` components.
95-
96-
## Effects
97-
98-
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:
99-
100-
```ts
101-
effect(() => {
102-
console.log(`The current count is: ${count()}`);
103-
});
104-
```
105-
106-
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.
92+
## Advanced derivations
10793

108-
Effects always execute **asynchronously**, during the change detection process.
94+
While `computed` handles simple readonly derivations, you might find youself needing a writable state that is dependant on other signals.
95+
For more information see the [Dependent state with linkedSignal](/guide/signals/linked-signal) guide.
10996

110-
### Use cases for effects
97+
All signal APIs are synchronous— `signal`, `computed`, `input`, etc. However, applications often need to deal with data that is available asynchronously. A `Resource` gives you a way to incorporate async data into your application's signal-based code and still allow you to access its data synchronously. For more information see the [Async reactivity with resources](/guide/signals/resource) guide.
11198

112-
Effects are rarely needed in most application code, but may be useful in specific circumstances. Here are some examples of situations where an `effect` might be a good solution:
99+
## Executing side effects on non-reactive APIs
113100

114-
- Logging data being displayed and when it changes, either for analytics or as a debugging tool.
115-
- Keeping data in sync with `window.localStorage`.
116-
- Adding custom DOM behavior that can't be expressed with template syntax.
117-
- Performing custom rendering to a `<canvas>`, charting library, or other third party UI library.
118-
119-
<docs-callout critical title="When not to use effects">
120-
Avoid using effects for propagation of state changes. This can result in `ExpressionChangedAfterItHasBeenChecked` errors, infinite circular updates, or unnecessary change detection cycles.
121-
122-
Instead, use `computed` signals to model state that depends on other state.
123-
</docs-callout>
124-
125-
### Injection context
126-
127-
By default, you can only create an `effect()` within an [injection context](guide/di/dependency-injection-context) (where you have access to the [`inject`](/api/core/inject) function). The easiest way to satisfy this requirement is to call `effect` within a component, directive, or service `constructor`:
128-
129-
```ts
130-
@Component({...})
131-
export class EffectiveCounterComponent {
132-
readonly count = signal(0);
133-
constructor() {
134-
// Register a new effect.
135-
effect(() => {
136-
console.log(`The count is: ${this.count()}`);
137-
});
138-
}
139-
}
140-
```
141-
142-
Alternatively, you can assign the effect to a field (which also gives it a descriptive name).
143-
144-
```ts
145-
@Component({...})
146-
export class EffectiveCounterComponent {
147-
readonly count = signal(0);
101+
Synchronous or asynchronous derivations are recommended when we want to react to state changes. However this doesn't cover all the usecase and you'll sometime find yourself in a situation where you need to react on signal changes on non-reactive apis. Use `effect` or `afterRenderEffect` for those specific usecases. For more information see [Side effects for non-reactives APIs](/guide/effect) guide.
148102

149-
private loggingEffect = effect(() => {
150-
console.log(`The count is: ${this.count()}`);
151-
});
152-
}
153-
```
154-
155-
To create an effect outside the constructor, you can pass an `Injector` to `effect` via its options:
156-
157-
```ts
158-
@Component({...})
159-
export class EffectiveCounterComponent {
160-
readonly count = signal(0);
161-
private injector = inject(Injector);
162-
163-
initializeLogging(): void {
164-
effect(() => {
165-
console.log(`The count is: ${this.count()}`);
166-
}, {injector: this.injector});
167-
}
168-
}
169-
```
170-
171-
### Destroying effects
172-
173-
When you create an effect, it is automatically destroyed when its enclosing context is destroyed. This means that effects created within components are destroyed when the component is destroyed. The same goes for effects within directives, services, etc.
103+
## Reading signals in `OnPush` components
174104

175-
Effects return an `EffectRef` that you can use to destroy them manually, by calling the `.destroy()` method. You can combine this with the `manualCleanup` option to create an effect that lasts until it is manually destroyed. Be careful to actually clean up such effects when they're no longer required.
105+
When you read a signal within an `OnPush` component's template, Angular tracks the signal as a dependency of that component. When the value of that signal changes, Angular automatically [marks](api/core/ChangeDetectorRef#markforcheck) the component to ensure it gets updated the next time change detection runs. Refer to the [Skipping component subtrees](best-practices/skipping-subtrees) guide for more information about `OnPush` components.
176106

177107
## Advanced topics
178108

@@ -253,24 +183,6 @@ effect(() => {
253183
});
254184
```
255185

256-
### Effect cleanup functions
257-
258-
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.
259-
260-
```ts
261-
effect((onCleanup) => {
262-
const user = currentUser();
263-
264-
const timer = setTimeout(() => {
265-
console.log(`1 second ago, the user became ${user}`);
266-
}, 1000);
267-
268-
onCleanup(() => {
269-
clearTimeout(timer);
270-
});
271-
});
272-
```
273-
274186
## Using signals with RxJS
275187

276188
See [RxJS interop with Angular signals](ecosystem/rxjs-interop) for details on interoperability between signals and RxJS.

adev/src/content/guide/signals/resource.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
IMPORTANT: `resource` is [experimental](reference/releases#experimental). It's ready for you to try, but it might change before it is stable.
44

5-
Most signal APIs are synchronous— `signal`, `computed`, `input`, etc. However, applications often need to deal with data that is available asynchronously. A `Resource` gives you a way to incorporate async data into your application's signal-based code.
5+
All signal APIs are synchronous— `signal`, `computed`, `input`, etc. However, applications often need to deal with data that is available asynchronously. A `Resource` gives you a way to incorporate async data into your application's signal-based code and still allow you to access its data synchronously.
66

77
You can use a `Resource` to perform any kind of async operation, but the most common use-case for `Resource` is fetching data from a server. The following example creates a resource to fetch some user data.
88

0 commit comments

Comments
 (0)