From 39a3e34e035b324bfa1f1a11cf452272c91011c6 Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Wed, 20 Sep 2023 15:27:56 +0200 Subject: [PATCH] fix(core): allow toSignal calls in reactive context (#51831) (#51892) This PR moves the Observable subscription of toSignal outside of the reactive context. As the result the toSignal calls are allowed in the computed, effect and all other reactive consumers. This is based on the reasoning that we already allow signals creation in a reactive context. Plus a similar change was done to the async pipe in the https://github.com/angular/angular/pull/50522 Fixes #51027 PR Close #51892 --- packages/core/rxjs-interop/src/to_signal.ts | 30 ++++++++++--------- .../core/rxjs-interop/test/to_signal_spec.ts | 15 +++++++++- packages/core/test/signals/computed_spec.ts | 11 ++++++- 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/packages/core/rxjs-interop/src/to_signal.ts b/packages/core/rxjs-interop/src/to_signal.ts index 36b2faa32c299..f5ff3f3c89609 100644 --- a/packages/core/rxjs-interop/src/to_signal.ts +++ b/packages/core/rxjs-interop/src/to_signal.ts @@ -178,21 +178,23 @@ export function toSignal( state = signal>({kind: StateKind.Value, value: options?.initialValue as U}); } - const sub = source.subscribe({ - next: value => state.set({kind: StateKind.Value, value}), - error: error => state.set({kind: StateKind.Error, error}), - // Completion of the Observable is meaningless to the signal. Signals don't have a concept of - // "complete". - }); - - if (ngDevMode && options?.requireSync && untracked(state).kind === StateKind.NoValue) { - throw new RuntimeError( - RuntimeErrorCode.REQUIRE_SYNC_WITHOUT_SYNC_EMIT, - '`toSignal()` called with `requireSync` but `Observable` did not emit synchronously.'); - } + untracked(() => { + const sub = source.subscribe({ + next: value => state.set({kind: StateKind.Value, value}), + error: error => state.set({kind: StateKind.Error, error}), + // Completion of the Observable is meaningless to the signal. Signals don't have a concept of + // "complete". + }); + + if (ngDevMode && options?.requireSync && state().kind === StateKind.NoValue) { + throw new RuntimeError( + RuntimeErrorCode.REQUIRE_SYNC_WITHOUT_SYNC_EMIT, + '`toSignal()` called with `requireSync` but `Observable` did not emit synchronously.'); + } - // Unsubscribe when the current context is destroyed, if requested. - cleanupRef?.onDestroy(sub.unsubscribe.bind(sub)); + // Unsubscribe when the current context is destroyed, if requested. + cleanupRef?.onDestroy(sub.unsubscribe.bind(sub)); + }); // The actual returned signal is a `computed` of the `State` signal, which maps the various states // to either values or errors. diff --git a/packages/core/rxjs-interop/test/to_signal_spec.ts b/packages/core/rxjs-interop/test/to_signal_spec.ts index c3ec6c59ce8d2..41f14594aed3c 100644 --- a/packages/core/rxjs-interop/test/to_signal_spec.ts +++ b/packages/core/rxjs-interop/test/to_signal_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {DestroyRef, EnvironmentInjector, Injector, runInInjectionContext} from '@angular/core'; +import {computed, EnvironmentInjector, Injector, runInInjectionContext} from '@angular/core'; import {toSignal} from '@angular/core/rxjs-interop'; import {BehaviorSubject, ReplaySubject, Subject} from 'rxjs'; @@ -109,6 +109,19 @@ describe('toSignal()', () => { expect(counter()).toBe(1); }); + it('should allow toSignal creation in a reactive context - issue 51027', () => { + const counter$ = new BehaviorSubject(1); + + const injector = Injector.create([]); + + const doubleCounter = computed(() => { + const counter = toSignal(counter$, {requireSync: true, injector}); + return counter() * 2; + }); + + expect(doubleCounter()).toBe(2); + }); + describe('with no initial value', () => { it('should return `undefined` if read before a value is emitted', test(() => { const counter$ = new Subject(); diff --git a/packages/core/test/signals/computed_spec.ts b/packages/core/test/signals/computed_spec.ts index aa5f03bb7971f..c2959ecea64f7 100644 --- a/packages/core/test/signals/computed_spec.ts +++ b/packages/core/test/signals/computed_spec.ts @@ -167,7 +167,16 @@ describe('computed', () => { expect(watchCount).toEqual(2); }); - it('should disallow writing to signals within computeds', () => { + it('should allow signal creation within computed', () => { + const doubleCounter = computed(() => { + const counter = signal(1); + return counter() * 2; + }); + + expect(doubleCounter()).toBe(2); + }); + + it('should disallow writing to signals within computed', () => { const source = signal(0); const illegal = computed(() => { source.set(1);