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);