Skip to content

Commit dcf18dc

Browse files
pkozlowski-opensourcedylhunn
authored andcommitted
fix(core): allow toSignal calls in reactive context (#51831)
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 #50522 Fixes #51027 PR Close #51831
1 parent 917203d commit dcf18dc

File tree

3 files changed

+40
-16
lines changed

3 files changed

+40
-16
lines changed

packages/core/rxjs-interop/src/to_signal.ts

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -175,21 +175,23 @@ export function toSignal<T, U = undefined>(
175175
state = signal<State<T|U>>({kind: StateKind.Value, value: options?.initialValue as U});
176176
}
177177

178-
const sub = source.subscribe({
179-
next: value => state.set({kind: StateKind.Value, value}),
180-
error: error => state.set({kind: StateKind.Error, error}),
181-
// Completion of the Observable is meaningless to the signal. Signals don't have a concept of
182-
// "complete".
183-
});
184-
185-
if (ngDevMode && options?.requireSync && untracked(state).kind === StateKind.NoValue) {
186-
throw new ɵRuntimeError(
187-
ɵRuntimeErrorCode.REQUIRE_SYNC_WITHOUT_SYNC_EMIT,
188-
'`toSignal()` called with `requireSync` but `Observable` did not emit synchronously.');
189-
}
178+
untracked(() => {
179+
const sub = source.subscribe({
180+
next: value => state.set({kind: StateKind.Value, value}),
181+
error: error => state.set({kind: StateKind.Error, error}),
182+
// Completion of the Observable is meaningless to the signal. Signals don't have a concept of
183+
// "complete".
184+
});
185+
186+
if (ngDevMode && options?.requireSync && state().kind === StateKind.NoValue) {
187+
throw new ɵRuntimeError(
188+
ɵRuntimeErrorCode.REQUIRE_SYNC_WITHOUT_SYNC_EMIT,
189+
'`toSignal()` called with `requireSync` but `Observable` did not emit synchronously.');
190+
}
190191

191-
// Unsubscribe when the current context is destroyed, if requested.
192-
cleanupRef?.onDestroy(sub.unsubscribe.bind(sub));
192+
// Unsubscribe when the current context is destroyed, if requested.
193+
cleanupRef?.onDestroy(sub.unsubscribe.bind(sub));
194+
});
193195

194196
// The actual returned signal is a `computed` of the `State` signal, which maps the various states
195197
// to either values or errors.

packages/core/rxjs-interop/test/to_signal_spec.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {DestroyRef, EnvironmentInjector, Injector, runInInjectionContext} from '@angular/core';
9+
import {computed, EnvironmentInjector, Injector, runInInjectionContext} from '@angular/core';
1010
import {toSignal} from '@angular/core/rxjs-interop';
1111
import {BehaviorSubject, ReplaySubject, Subject} from 'rxjs';
1212

@@ -109,6 +109,19 @@ describe('toSignal()', () => {
109109
expect(counter()).toBe(1);
110110
});
111111

112+
it('should allow toSignal creation in a reactive context - issue 51027', () => {
113+
const counter$ = new BehaviorSubject(1);
114+
115+
const injector = Injector.create([]);
116+
117+
const doubleCounter = computed(() => {
118+
const counter = toSignal(counter$, {requireSync: true, injector});
119+
return counter() * 2;
120+
});
121+
122+
expect(doubleCounter()).toBe(2);
123+
});
124+
112125
describe('with no initial value', () => {
113126
it('should return `undefined` if read before a value is emitted', test(() => {
114127
const counter$ = new Subject<number>();

packages/core/test/signals/computed_spec.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,16 @@ describe('computed', () => {
167167
expect(watchCount).toEqual(2);
168168
});
169169

170-
it('should disallow writing to signals within computeds', () => {
170+
it('should allow signal creation within computed', () => {
171+
const doubleCounter = computed(() => {
172+
const counter = signal(1);
173+
return counter() * 2;
174+
});
175+
176+
expect(doubleCounter()).toBe(2);
177+
});
178+
179+
it('should disallow writing to signals within computed', () => {
171180
const source = signal(0);
172181
const illegal = computed(() => {
173182
source.set(1);

0 commit comments

Comments
 (0)