Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion modules/signals/rxjs-interop/spec/rx-method.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ describe('rxMethod', () => {
[of(1), 'Observable'],
] as const) {
describe(`${name}`, () => {
it('warns when source injector is used', () => {
it('warns when source injector is root', () => {
let a = 1;
const adder = createAdder((value) => (a += value));
adder(reactiveValue);
Expand All @@ -414,6 +414,20 @@ describe('rxMethod', () => {
);
});

it('does not warn when source injector is not root', () => {
let a = 1;
const childInjector = createEnvironmentInjector(
[],
TestBed.inject(EnvironmentInjector)
);
const adder = runInInjectionContext(childInjector, () =>
rxMethod<number>(tap((value) => (a += value)))
);
adder(reactiveValue);

expect(warnSpy).not.toHaveBeenCalled();
});

it('does not warn on manual injector', () => {
let a = 1;
const adder = createAdder((value) => (a += value));
Expand Down
21 changes: 17 additions & 4 deletions modules/signals/rxjs-interop/src/rx-method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,15 @@ export function rxMethod<Input>(
}

const callerInjector = getCallerInjector();
const instanceInjector =
config?.injector ?? callerInjector ?? sourceInjector;

if (
typeof ngDevMode !== 'undefined' &&
ngDevMode &&
config?.injector === undefined &&
callerInjector === undefined
callerInjector === undefined &&
isRootInjector(sourceInjector)
) {
console.warn(
'@ngrx/signals/rxjs-interop: The reactive method was called outside',
Expand All @@ -96,9 +100,6 @@ export function rxMethod<Input>(
);
}

const instanceInjector =
config?.injector ?? callerInjector ?? sourceInjector;

if (typeof input === 'function') {
const watcher = effect(
() => {
Expand Down Expand Up @@ -139,3 +140,15 @@ function getCallerInjector(): Injector | undefined {
return undefined;
}
}

/**
* Checks whether the given injector is a root or platform injector.
*
* Uses the `scopes` property from Angular's `R3Injector` (the concrete
* `EnvironmentInjector` implementation) via duck typing. This is an
* internal Angular API that may change in future versions.
*/
function isRootInjector(injector: Injector): boolean {
const scopes: Set<string> | undefined = (injector as any)['scopes'];
return scopes?.has('root') === true || scopes?.has('platform') === true;
}
16 changes: 15 additions & 1 deletion modules/signals/spec/signal-method.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ describe('signalMethod', () => {
warnSpy.mockClear();
});

it('warns when source injector is used for a signal', () => {
it('warns when source injector is root', () => {
let a = 1;
const adder = createAdder((value) => (a += value));
adder(n);
Expand All @@ -260,6 +260,20 @@ describe('signalMethod', () => {
);
});

it('does not warn when source injector is not root', () => {
let a = 1;
const childInjector = createEnvironmentInjector(
[],
TestBed.inject(EnvironmentInjector)
);
const adder = runInInjectionContext(childInjector, () =>
signalMethod<number>((value) => (a += value))
);
adder(n);

expect(warnSpy).not.toHaveBeenCalled();
});

it('does not warn on non-reactive value and source injector', () => {
let a = 1;
const adder = createAdder((value) => (a += value));
Expand Down
21 changes: 17 additions & 4 deletions modules/signals/src/signal-method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,15 @@ export function signalMethod<Input>(
): EffectRef => {
if (isReactiveComputation(input)) {
const callerInjector = getCallerInjector();
const instanceInjector =
config?.injector ?? callerInjector ?? sourceInjector;

if (
typeof ngDevMode !== 'undefined' &&
ngDevMode &&
config?.injector === undefined &&
callerInjector === undefined
callerInjector === undefined &&
isRootInjector(sourceInjector)
) {
console.warn(
'@ngrx/signals: The function returned by signalMethod was called',
Expand All @@ -75,9 +79,6 @@ export function signalMethod<Input>(
);
}

const instanceInjector =
config?.injector ?? callerInjector ?? sourceInjector;

const watcher = effect(
() => {
const value = input();
Expand Down Expand Up @@ -118,3 +119,15 @@ function getCallerInjector(): Injector | undefined {
function isReactiveComputation<T>(value: T | (() => T)): value is () => T {
return typeof value === 'function';
}

/**
* Checks whether the given injector is a root or platform injector.
*
* Uses the `scopes` property from Angular's `R3Injector` (the concrete
* `EnvironmentInjector` implementation) via duck typing. This is an
* internal Angular API that may change in future versions.
*/
function isRootInjector(injector: Injector): boolean {
const scopes: Set<string> | undefined = (injector as any)['scopes'];
return scopes?.has('root') === true || scopes?.has('platform') === true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ export class Numbers implements OnInit {

<ngrx-docs-alert type="inform">

If the injector is not provided when calling the reactive method with a signal, a computation function, or an observable outside the injection context, a warning message about a potential memory leak is displayed in development mode.
If the reactive method is initialized in a root-scoped injector (e.g. a service with `providedIn: 'root'`) and called with a signal, a computation function, or an observable outside the injection context without providing an injector, a warning message about a potential memory leak is displayed in development mode. This warning does not apply when the reactive method is initialized in a non-root injector (e.g. a component or a service provided at the component level), since cleanup is handled by the source injector's lifecycle.

</ngrx-docs-alert>

Expand Down
2 changes: 1 addition & 1 deletion projects/www/src/app/pages/guide/signals/signal-method.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ Here, the `effect` used internally by `signalMethod` outlives the component, whi

<ngrx-docs-alert type="inform">

If an injector is not provided when a method generated by `signalMethod` is called with a signal outside the injection context, a warning message about a potential memory leak is displayed in development mode.
If `signalMethod` is initialized in a root-scoped injector (e.g. a service with `providedIn: 'root'`) and called with a signal outside the injection context without providing an injector, a warning message about a potential memory leak is displayed in development mode. This warning does not apply when `signalMethod` is initialized in a non-root injector (e.g. a component or a service provided at the component level), since cleanup is handled by the source injector's lifecycle.

</ngrx-docs-alert>

Expand Down