kamilkisiela / apollo-angular

A fully-featured, production ready caching GraphQL client for Angular and every GraphQL server 🎁
https://apollo-angular.com
MIT License
1.5k stars 310 forks source link

Subscriptions are not executed in NgZone #320

Closed ribizli closed 7 years ago

ribizli commented 7 years ago

If a subscription event comes, the store is updated, the observable emits, but no change detection is running in angular. (I need to interact with a component to get the view being updated)

// service
const gameChanges = gql`
  subscription GameChanges {
    gameChanged {
      ...GameFragment
    }
  }
  ${UserFragment}
  ${GameFragment}
`;

this.gameChanges$ = apollo.subscribe({
      query: gameChanges
    }).map((r: { gameChanged: Game }) => r.gameChanged)

// component
this._gameService.gameChanges$.subscribe(g => console.log(`game changed: ${g.id}`));
ribizli commented 7 years ago

My current workaround for this is to delay the emission into the next event loop (???):

this._gameService.gameChanges$.delay(0).subscribe(g => console.log(`game changed: ${g.id}`));
eitanfr commented 7 years ago

apollo.subsribe is wraped with zone current zone , so change detection should be invoked. can you show the components code? do you use ON_PUSH?

ribizli commented 7 years ago

No ON_PUSH at all. My other comp uses watchQuery with async. I expect it's view to get updated automatically but it won't. More code sample I can provide later on... UPDATE: added code

// game service
@Injectable()
export class GameService {
  gameChanges$: Observable<Game>;

  constructor(private _apollo: Apollo) {
    this.gameChanges$ = _apollo.subscribe({
      query: gameChanges
    }).map((r: { gameChanged: Game }) => r.gameChanged);
  }

  game(id: number) {
    return this._apollo.watchQuery<GameResult>({
      query: gameQuery,
      variables: { id }
    }).map(r => r.data && r.data.game);
  }
}

// game component
@Component({
  template: `
    <pre *ngIf="game$ | async as game">
      {{game | json}}
    </pre>
  `
})
export class GameComponent {

  game$: Observable<Game>;

  constructor(private _gameService: GameService, _route: ActivatedRoute) {
    const id$ = _route.params.map(p => +p['id']);
    this.game$ = id$.switchMap(id => _gameService.game(id));
  }

}

In the OP I've also shown the parent component (it's routing child is the game component) with the subscription. From the observables I get all the emitted data correct, I also checked the Apollo Client's cache, everything is updated but the view. After the next CD, the view is also updated. And the mentioned workaround is also working (don't know why).

alansmithnbs commented 7 years ago

I am also seeing this issue on our project. Delay didn't work for me, instead Apollo subscription results seem to have to be wrapped in a zone.run(() => { ... }) to get Angular templates to update.

We are using stock change detection (i.e NO ON_PUSH) Could this also be related to #295?

ribizli commented 7 years ago

I can again confirm, that this bug is still valid.

I've tested it with NgZone.isInAngularZone() in the next() handler, and any emission caused by an Apollo Client cache update is outside of NgZone.

Again a short description: I use watchQuery to query an object. The first result is emitted in NgZone. Later a GraphQL Subscription sends an update of this object. The emission from the subscription's Observable is also correctly in NgZone. But the caused cache update in the client's cache triggers an emission of the watchQuery which is not in NgZone anymore.

My workaround is really to delay(0) the subscription emission, which I assume starts a change detection right after the cache update emission already happened.

ribizli commented 7 years ago

Workaround No.2 - wrapping the watchQuery observable into NgZone:

function observeOnZone<T>(this: Observable<T>, zone: NgZone): Observable<T> {
    return Observable.create(observer => {
        const onNext = (value) => zone.run(() => observer.next(value));
        const onError = (e) => zone.run(() => observer.error(e));
        const onComplete = () => zone.run(() => observer.complete());
        return this.subscribe(onNext, onError, onComplete);
    });
};

Observable.prototype.observeOnZone = observeOnZone;

declare module 'rxjs/Observable' {
  interface Observable<T> {
    observeOnZone: typeof observeOnZone;
  }
}

// in service

this._apollo.watchQuery<CurrentPlaylistQueryResult>({
  query: currentPlaylistQuery
}).map(r => r.data.currentPlaylist)
  .observeOnZone(this._ngZone);
kamilkisiela commented 7 years ago

works in apollo-angular@1.0.0