Skip to content

Commit

Permalink
fix(core): allow toSignal calls in reactive context (#51831) (#51892)
Browse files Browse the repository at this point in the history
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 #51892
  • Loading branch information
pkozlowski-opensource authored and dylhunn committed Sep 27, 2023
1 parent aedd8de commit 39a3e34
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 16 deletions.
30 changes: 16 additions & 14 deletions packages/core/rxjs-interop/src/to_signal.ts
Expand Up @@ -178,21 +178,23 @@ export function toSignal<T, U = undefined>(
state = signal<State<T|U>>({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.
Expand Down
15 changes: 14 additions & 1 deletion packages/core/rxjs-interop/test/to_signal_spec.ts
Expand Up @@ -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';

Expand Down Expand Up @@ -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<number>();
Expand Down
11 changes: 10 additions & 1 deletion packages/core/test/signals/computed_spec.ts
Expand Up @@ -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);
Expand Down

0 comments on commit 39a3e34

Please sign in to comment.