GraphQL订阅:组件多次进入后单次变更触发多次接收问题
Hey Nick, this is a super common gotcha when working with Angular + Apollo Client subscriptions—let me break down what's happening and how to fix it!
Every time you navigate into your Angular component, you're likely initializing a new GraphQL subscription (either via apollo.subscribe() or subscribeToMore). But when you leave the component, those old subscription instances aren't being cleaned up. They stay active in the background, so when a MongoDB change triggers a subscription push, all those leftover subscriptions fire at once—hence the n duplicate updates matching how many times you've entered the component.
1. Use RxJS takeUntil (Most Recommended Angular Pattern)
Create a "destroy signal" Subject in your component, and pipe all subscriptions through takeUntil to auto-cancel them when the component is destroyed.
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Apollo } from 'apollo-angular'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { YOUR_DATA_UPDATES_SUBSCRIPTION } from './your-graphql-queries'; @Component({ selector: 'app-your-component', templateUrl: './your-component.component.html' }) export class YourComponent implements OnInit, OnDestroy { // Create a subject to emit when the component is destroyed private destroy$ = new Subject<void>(); constructor(private apollo: Apollo) {} ngOnInit(): void { this.apollo.subscribe({ query: YOUR_DATA_UPDATES_SUBSCRIPTION }) .pipe(takeUntil(this.destroy$)) // Auto-unsubscribe when destroy$ emits .subscribe({ next: (data) => { console.log('Received fresh data:', data); // Update your component state here }, error: (err) => console.error('Subscription error:', err) }); } ngOnDestroy(): void { // Trigger the destroy signal and clean up the subject this.destroy$.next(); this.destroy$.complete(); } }
2. Manually Unsubscribe with Subscription References
If you're using subscribeToMore alongside a watchQuery, save the returned Subscription object and call unsubscribe() in ngOnDestroy:
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Apollo, Subscription } from 'apollo-angular'; import { YOUR_LIST_QUERY, YOUR_UPDATES_SUBSCRIPTION } from './your-graphql-queries'; @Component({...}) export class YourComponent implements OnInit, OnDestroy { private subscription?: Subscription; constructor(private apollo: Apollo) {} ngOnInit(): void { const dataQuery = this.apollo.watchQuery({ query: YOUR_LIST_QUERY }); // Save the subscribeToMore subscription reference this.subscription = dataQuery.subscribeToMore({ document: YOUR_UPDATES_SUBSCRIPTION, updateQuery: (prevData, { subscriptionData }) => { // Your cache update logic here return updatedData; } }); } ngOnDestroy(): void { // Manually cancel the subscription if (this.subscription) { this.subscription.unsubscribe(); } } }
3. Double-Check Async Pipe Usage
If you're using Angular's async pipe in your template to handle the subscription, Angular should auto-manage cleanup—but only if you're not re-creating the Observable on every change detection cycle. Avoid calling a method that returns an Observable directly in the template (e.g., {{ getSubscription() | async }}). Instead, assign the Observable to a component property first:
// In component dataUpdates$ = this.apollo.subscribe({ query: YOUR_UPDATES_SUBSCRIPTION }); // In template <div *ngFor="let item of dataUpdates$ | async">{{ item.name }}</div>
While the issue is almost certainly client-side subscription leaks, you can quickly rule out a server problem by checking your MongoDB Change Stream setup. Ensure you're not creating multiple Change Stream instances per client connection—your server should only initialize one Change Stream per subscription request, and clean it up when the client disconnects.
内容的提问来源于stack exchange,提问作者Nick

