From 3e6b853a6b6b4589a1c17b61d0f25fd574f6fb01 Mon Sep 17 00:00:00 2001 From: LuEsc Date: Fri, 5 Jun 2026 21:24:41 -0500 Subject: [PATCH 1/5] docs(core): add signals mental model guidance --- adev/src/content/guide/signals/overview.md | 90 ++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/adev/src/content/guide/signals/overview.md b/adev/src/content/guide/signals/overview.md index a61c43a07478..31697be8c205 100644 --- a/adev/src/content/guide/signals/overview.md +++ b/adev/src/content/guide/signals/overview.md @@ -124,6 +124,65 @@ If you set `showCount` to `true` and then read `conditionalCount` again, the der 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`. +## Mental model + +Signals are best understood as a reactive state graph. + +Each signal represents a piece of state. Computed values derive state from other state. Effects react to state changes and interact with the outside world. + +A useful rule of thumb is: + +- Use `signal` for state. +- Use `computed` for derived state. +- Use `effect` for side effects. + +When working with signals, prefer deriving values over synchronizing values. If one value can be calculated from another, represent that relationship with `computed` instead of manually keeping multiple signals in sync. + +### Prefer derivation over synchronization + +When a value can be expressed as a function of other state, model it as a derivation instead of maintaining multiple sources of truth. + +Prefer: + +```ts +const fullName = computed(() => `${firstName()} ${lastName()}`); +``` + +Over: + +```ts +const fullName = signal(''); + +effect(() => { + fullName.set(`${firstName()} ${lastName()}`); +}); +``` + +Derived state stays consistent automatically and avoids unnecessary mutable state. + +### Choosing between computed and effect + +Both `computed` and `effect` react to signal changes, but they serve different purposes. + +Use `computed` when you need to calculate a value from other signals: + +```ts +const total = computed(() => price() * quantity()); +``` + +Use `effect` when you need to interact with APIs, browser storage, logging, analytics, or other non-reactive systems: + +```ts +effect(() => { + localStorage.setItem('theme', theme()); +}); +``` + +A simple guideline is: + +- If you're calculating a value, use `computed`. +- If you're causing something to happen, use `effect`. + ## Reactive contexts A **reactive context** is a runtime state where Angular monitors signal reads to establish a dependency. The code reading the signal is the _consumer_, and the signal being read is the _producer_. @@ -274,6 +333,37 @@ isWritableSignal(count); // true isWritableSignal(doubled); // false ``` +## Signals and RxJS + +Signals and RxJS solve different problems and are often used together. +Signals model state and relationships between values. +RxJS models events and asynchronous streams over time. + +A signal answers: + +> What is the current value? + +An Observable answers: + +> What values may be emitted over time? + +Signals are typically a good fit for: + +- Component state +- UI state +- Derived state +- Relationships between values + +RxJS is typically a better fit for: + +- Event streams +- WebSocket messages +- Request orchestration +- Cancellation and retry logic +- Complex asynchronous workflows + +Signals are not intended to replace RxJS. Instead, they provide a simpler model for representing and deriving application state. + ## Using signals with RxJS See [RxJS interop with Angular signals](ecosystem/rxjs-interop) for details on interoperability between signals and RxJS. From 321560237994127b4e698fe3b9ac42d26e237810 Mon Sep 17 00:00:00 2001 From: Luis Luna <34717264+LuEsc@users.noreply.github.com> Date: Mon, 8 Jun 2026 13:17:10 -0500 Subject: [PATCH 2/5] docs(core): clarify signals guidelines wording Co-authored-by: Ben Hong --- adev/src/content/guide/signals/overview.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adev/src/content/guide/signals/overview.md b/adev/src/content/guide/signals/overview.md index 31697be8c205..cce51f044e55 100644 --- a/adev/src/content/guide/signals/overview.md +++ b/adev/src/content/guide/signals/overview.md @@ -170,7 +170,7 @@ Use `computed` when you need to calculate a value from other signals: const total = computed(() => price() * quantity()); ``` -Use `effect` when you need to interact with APIs, browser storage, logging, analytics, or other non-reactive systems: +Use `effect` when you need to update something outside Angular’s signal model, such as browser storage, logging, analytics, or a third-party UI library. ```ts effect(() => { From 157832f0515a2fade72d0a32f6c9e2d433728a5e Mon Sep 17 00:00:00 2001 From: Luis Luna <34717264+LuEsc@users.noreply.github.com> Date: Mon, 8 Jun 2026 13:19:16 -0500 Subject: [PATCH 3/5] docs(core): refine effect and computed recommendations Co-authored-by: Ben Hong --- adev/src/content/guide/signals/overview.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adev/src/content/guide/signals/overview.md b/adev/src/content/guide/signals/overview.md index cce51f044e55..3dbeb2d5fc62 100644 --- a/adev/src/content/guide/signals/overview.md +++ b/adev/src/content/guide/signals/overview.md @@ -181,7 +181,7 @@ effect(() => { A simple guideline is: - If you're calculating a value, use `computed`. -- If you're causing something to happen, use `effect`. +- If you need to synchronize signal state to an external non-reactive system, use `effect`. ## Reactive contexts From 4ee07e6f25f24e1855cc0d2a9c7a745728a34bab Mon Sep 17 00:00:00 2001 From: Luis Luna <34717264+LuEsc@users.noreply.github.com> Date: Mon, 8 Jun 2026 13:19:31 -0500 Subject: [PATCH 4/5] docs(core): address review feedback Co-authored-by: Ben Hong --- adev/src/content/guide/signals/overview.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adev/src/content/guide/signals/overview.md b/adev/src/content/guide/signals/overview.md index 3dbeb2d5fc62..df5a6f6a586c 100644 --- a/adev/src/content/guide/signals/overview.md +++ b/adev/src/content/guide/signals/overview.md @@ -178,7 +178,7 @@ effect(() => { }); ``` -A simple guideline is: +In general, you'll want to follow the general guidelines: - If you're calculating a value, use `computed`. - If you need to synchronize signal state to an external non-reactive system, use `effect`. From caf362b9d52f3495bb478ef834e5f75d831224b2 Mon Sep 17 00:00:00 2001 From: LuEsc Date: Tue, 16 Jun 2026 11:18:09 -0500 Subject: [PATCH 5/5] docs(core): add common mistakes section to effect guide --- adev/src/content/guide/signals/effect.md | 71 ++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/adev/src/content/guide/signals/effect.md b/adev/src/content/guide/signals/effect.md index 0f02c35587d1..fc0f131ed39d 100644 --- a/adev/src/content/guide/signals/effect.md +++ b/adev/src/content/guide/signals/effect.md @@ -31,6 +31,77 @@ Avoid using effects for propagation of state changes. This can result in `Expres Instead, use `computed` signals to model state that depends on other state. +### Common mistakes + +#### Avoid using effects for derived state + +Effects run **asynchronously**, after Angular's change detection cycle. Setting a signal inside an effect means the new value won't be visible until the next cycle — any code that reads that signal in the same frame will see a stale value. + +Avoid: + +```ts +const total = signal(0); + +effect(() => { + total.set(price() * quantity()); +}); +``` + +Here `total` is a writable signal that tries to mirror `price() * quantity()`, but it always lags one cycle behind. It can also get out of sync if anything else writes to `total` directly. + +Prefer `computed`, which recalculates synchronously and lazily — the value is always fresh when read and can never drift from its sources: + +```ts +const total = computed(() => price() * quantity()); +``` + +As a general rule: if a value can be derived from other signals, it should be a `computed`, not a signal updated by an effect. + +#### Avoid using effects to synchronize state + +Using an effect to copy one signal's value into another creates two independent pieces of state representing the same data. This makes the relationship between them implicit and easy to break. + +Avoid: + +```ts +effect(() => { + selectedUserId.set(selectedUser()?.id); +}); +``` + +`selectedUserId` is a separate writable signal that can be set from anywhere else in the code, silently breaking its dependency on `selectedUser`. The reactive graph now has a hidden invariant that nothing enforces. + +Prefer deriving the value with `computed`, which makes the relationship explicit and always consistent: + +```ts +const selectedUserId = computed(() => selectedUser()?.id); +``` + +#### Keep effects focused on side effects + +Effects are designed for **one-way communication** from Angular's reactive state to external, non-reactive systems. They are the right tool when you need to react to a signal change and call something outside the reactive graph. + +Common examples: + +- Logging and analytics +- Browser APIs such as `localStorage`, `sessionStorage`, or cookies +- Canvas rendering and charting libraries +- Third-party UI libraries +- Custom DOM integrations + +```ts +effect(() => { + localStorage.setItem('theme', theme()); +}); +``` + +A useful rule of thumb when choosing between `computed` and `effect`: + +- Use `computed` to **calculate a value** — something that other parts of your app will read. +- Use `effect` to **trigger an action** — something that happens outside the reactive graph. + +If you can express what you need as a `computed`, that is almost always the better choice. + ### Injection context 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`: