Skip to content

Conversation

@david-shortman
Copy link
Contributor

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

[x] Bugfix
[ ] Feature
[ ] Code style update (formatting, local variables)
[ ] Refactoring (no functional changes, no api changes)
[ ] Build related changes
[ ] CI related changes
[ ] Documentation content changes
[ ] Other... Please describe:

What is the current behavior?

rxMethod and signalMethod warn about a potential memory leak whenever a reactive input (signal, observable, or computation function) is passed outside an injection context — even when the source injector (captured at creation time) will properly handle cleanup. This produces false-positive warnings in common patterns such as:

@Component({
  template: `<button (click)="example()" />`
})
export class ExampleComponent {
  someMethod = rxMethod<string>(pipe(tap(console.log)));
  value = signal('arbitrary');

  example() {
    this.someMethod(this.value); // ⚠️ False warning — component injector handles cleanup
  }
}

The warning is also inconsistent: it fires for scenario 2 (matrix below; component method call) but not scenario 4 (root service constructor call), even though scenario 4 is the one with actual leak potential.

Closes #4991

What is the new behavior?

The memory leak warning now only fires when all three conditions are met:

  1. No explicit injector is provided via the config parameter
  2. No caller injection context is available
  3. The source injector is a root or platform injector (long-lived)

This correctly suppresses the warning when rxMethod/signalMethod is initialized in a non-root injector (component or scoped service), since the source injector's lifecycle will handle cleanup.

The detection uses duck-typed access to Angular's internal R3Injector.scopes property. This is documented in code as an internal Angular API that may change in future versions.

Scenario matrix

# Scenario Real risk? Warned before Warns now
1 Component, constructor call No No No
2 Component, event handler call No Yes (false positive) No
3 Root service, method call Yes Yes Yes
4 Root service, constructor call Yes No No
5 Scoped service, constructor call No No No
6 Scoped service, method call No Yes (false positive) No

Does this PR introduce a breaking change?

[ ] Yes
[x] No

Other information

  • The isRootInjector helper uses R3Injector.scopes (Angular internal) via duck typing to avoid importing ɵR3Injector. This is intentionally documented as relying on an internal API.
  • Both rxMethod (in @ngrx/signals/rxjs-interop) and signalMethod (in @ngrx/signals) were updated with the same fix.
  • Documentation for both features was updated to clarify the narrower warning scope.

The rxMethod and signalMethod memory leak warning now only fires when
the source injector is a root or platform injector. Previously, the
warning fired any time a reactive input was passed outside an injection
context, even when the source injector (e.g. a component injector)
would properly handle cleanup.

Uses duck-typed access to Angular's internal R3Injector.scopes property
to detect root/platform injectors.

Closes ngrx#4991

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@netlify
Copy link

netlify bot commented Jan 30, 2026

Deploy Preview for ngrx-io ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit da5b706
🔍 Latest deploy log https://app.netlify.com/projects/ngrx-io/deploys/697c3f5d0ba0670008f53a45
😎 Deploy Preview https://deploy-preview-5079--ngrx-io.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@markostanimirovic
Copy link
Member

Hey @david-shortman 👋

Thanks for raising these PRs. I'm glad to see your PRs again!

I don't think we should show the warning only for root/platform injectors. In the following situation:

@Injectable()
export class ParentService {
  readonly parentMethod = rxMethod(...);
}

@Component({
  template: `
    @if (show()) { <child /> }
  `,
  providers: [ParentService],
})
export class ParentComponent {
  readonly show = signal(false);
}

@Component({ /* ... */ })
export class ChildComponent implements OnInit {
  readonly parentService = inject(ParentService);
  
  ngOnInit() {
    this.parentService.parentMethod(interval(2_000));
  }
}

The memory leak will happen when ChildComponent is destroyed, but a warning won't be displayed.

Another problem with this implementation is using the private injector.scope property that can potentially change in a non-major Angular version without a breaking change notice.

@david-shortman
Copy link
Contributor Author

david-shortman commented Feb 2, 2026

I'll see if I can detect this situation. Additionally, I can add testing that would catch a breaking change from Angular.

Glad to be looking at NgRx again. I was curious to look at older issues I had been involved with but with the fresh lens of researching and validating ideas with Claude Code.

@rainerhahnekamp
Copy link
Contributor

👋 Hey all. If we’re looking to make changes here, I’d actually propose going into the opposite direction: show a deprecation message instead of a warning.

In that sense, rxMethod would align much better with the other Angular signal-based functions like effect, linkedSignal, and the resource APIs.

@david-shortman
Copy link
Contributor Author

david-shortman commented Feb 3, 2026

👋 Hey all. If we’re looking to make changes here, I’d actually propose going into the opposite direction: show a deprecation message instead of a warning.

In that sense, rxMethod would align much better with the other Angular signal-based functions like effect, linkedSignal, and the resource APIs.

You're referring to requiring supplying the injection context explicitly in all cases, correct?

Although I like the stylishness of the context being implicitly loaded, and the slightly less boilerplate that comes with it, I would support either direction.

So you're saying that, like the other Angular signal functions, unless you call in an injection context like the constructor, then you must explicitly supply the intended injection context?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

rxMethod warning about memory leak when none is possible

3 participants