From 7e4a60918026373de18ba5357835f43aa1994e8d Mon Sep 17 00:00:00 2001 From: Adrien Crivelli Date: Mon, 13 Oct 2025 14:53:35 +0900 Subject: [PATCH] New `onlyComplete()` helper to filter only complete results If you use this, you should probably combine it with [`notifyOnNetworkStatusChange`](https://www.apollographql.com/docs/react/data/queries#queryhookoptions-interface-notifyonnetworkstatuschange). This tells `@apollo/client` to not emit the first `partial` result, so `apollo-angular` does not need to filter it out. The overall behavior is identical, but it saves some CPU cycles. So something like this: ```ts apollo .watchQuery({ query: myQuery, notifyOnNetworkStatusChange: false, // Adding this will save CPU cycles }) .valueChanges .pipe(onlyComplete()) .subscribe(result => { // Do something with complete result }); ``` --- .changeset/olive-dogs-watch.md | 25 ++++++++ packages/apollo-angular/src/index.ts | 1 + packages/apollo-angular/src/only-complete.ts | 36 ++++++++++++ .../testing/tests/only-complete.spec.ts | 57 +++++++++++++++++++ packages/demo/src/app/app.component.ts | 3 + .../app/pages/movie/movie-page.component.ts | 9 +-- .../app/pages/movies/movies-page.component.ts | 9 +-- 7 files changed, 132 insertions(+), 8 deletions(-) create mode 100644 .changeset/olive-dogs-watch.md create mode 100644 packages/apollo-angular/src/only-complete.ts create mode 100644 packages/apollo-angular/testing/tests/only-complete.spec.ts diff --git a/.changeset/olive-dogs-watch.md b/.changeset/olive-dogs-watch.md new file mode 100644 index 000000000..012e02cd5 --- /dev/null +++ b/.changeset/olive-dogs-watch.md @@ -0,0 +1,25 @@ +--- +'apollo-angular': minor +--- + +New `onlyComplete()` helper to filter only complete results + +If you use this, you should probably combine it with [`notifyOnNetworkStatusChange`](https://www.apollographql.com/docs/react/data/queries#queryhookoptions-interface-notifyonnetworkstatuschange). +This tells `@apollo/client` to not emit the first `partial` result, so +`apollo-angular` does not need to filter it out. The overall behavior is +identical, but it saves some CPU cycles. + +So something like this: + +```ts +apollo + .watchQuery({ + query: myQuery, + notifyOnNetworkStatusChange: false, // Adding this will save CPU cycles + }) + .valueChanges + .pipe(onlyComplete()) + .subscribe(result => { + // Do something with complete result + }); +``` diff --git a/packages/apollo-angular/src/index.ts b/packages/apollo-angular/src/index.ts index 1fb141f72..2cf00e6f6 100644 --- a/packages/apollo-angular/src/index.ts +++ b/packages/apollo-angular/src/index.ts @@ -8,3 +8,4 @@ export { Subscription } from './subscription'; export { APOLLO_OPTIONS, APOLLO_NAMED_OPTIONS, APOLLO_FLAGS } from './tokens'; export type { Flags, NamedOptions, ResultOf, VariablesOf } from './types'; export { gql } from './gql'; +export { onlyComplete } from './only-complete'; diff --git a/packages/apollo-angular/src/only-complete.ts b/packages/apollo-angular/src/only-complete.ts new file mode 100644 index 000000000..b3bc8cbd5 --- /dev/null +++ b/packages/apollo-angular/src/only-complete.ts @@ -0,0 +1,36 @@ +import { filter, type OperatorFunction } from 'rxjs'; +import type { ObservableQuery } from '@apollo/client/core'; + +/** + * Filter emitted results to only receive results that are complete (`result.dataState === 'complete'`). + * + * This is a small wrapper around rxjs `filter()` for convenience only. + * + * If you use this, you should probably combine it with [`notifyOnNetworkStatusChange`](https://www.apollographql.com/docs/react/data/queries#queryhookoptions-interface-notifyonnetworkstatuschange). + * This tells `@apollo/client` to not emit the first `partial` result, so `apollo-angular` does + * not need to filter it out. The overall behavior is identical, but it saves some CPU cycles. + * + * So something like this: + * + * ```ts + * apollo + * .watchQuery({ + * query: myQuery, + * notifyOnNetworkStatusChange: false, // Adding this will save CPU cycles + * }) + * .valueChanges + * .pipe(onlyComplete()) + * .subscribe(result => { + * // Do something with complete result + * }); + * ``` + */ +export function onlyComplete(): OperatorFunction< + ObservableQuery.Result, + ObservableQuery.Result +> { + return filter( + (result): result is ObservableQuery.Result => + result.dataState === 'complete', + ); +} diff --git a/packages/apollo-angular/testing/tests/only-complete.spec.ts b/packages/apollo-angular/testing/tests/only-complete.spec.ts new file mode 100644 index 000000000..dfb9689e6 --- /dev/null +++ b/packages/apollo-angular/testing/tests/only-complete.spec.ts @@ -0,0 +1,57 @@ +import { onlyComplete } from 'apollo-angular'; +import { Subject } from 'rxjs'; +import { describe, expect, test } from 'vitest'; +import { NetworkStatus, ObservableQuery } from '@apollo/client/core'; + +interface Result { + user: { + name: string; + }; +} + +describe('onlyComplete', () => { + let theUser: Result['user'] | null = null; + let count = 0; + + test('should receive only complete results', () => + new Promise(done => { + const b = new Subject>(); + b.pipe(onlyComplete()).subscribe({ + next: result => { + count++; + theUser = result.data.user; + }, + complete: () => { + expect(count).toBe(1); + expect(theUser).toEqual({ name: 'foo' }); + done(); + }, + }); + + b.next({ + dataState: 'partial', + data: {}, + loading: true, + partial: true, + networkStatus: NetworkStatus.loading, + } satisfies ObservableQuery.Result); + + b.next({ + dataState: 'complete', + data: { user: { name: 'foo' } }, + loading: false, + partial: false, + networkStatus: NetworkStatus.ready, + } satisfies ObservableQuery.Result); + + b.next({ + dataState: 'partial', + data: {}, + loading: true, + partial: true, + networkStatus: NetworkStatus.loading, + } satisfies ObservableQuery.Result); + + b.complete(); + })); +}); diff --git a/packages/demo/src/app/app.component.ts b/packages/demo/src/app/app.component.ts index b5e4a3951..fc8d3f6e7 100644 --- a/packages/demo/src/app/app.component.ts +++ b/packages/demo/src/app/app.component.ts @@ -1,5 +1,8 @@ +import { Apollo, gql, onlyComplete } from 'apollo-angular'; +import { Subject } from 'rxjs'; import { Component } from '@angular/core'; import { RouterLink, RouterOutlet } from '@angular/router'; +import type { ObservableQuery } from '@apollo/client/core'; @Component({ selector: 'app-root', diff --git a/packages/demo/src/app/pages/movie/movie-page.component.ts b/packages/demo/src/app/pages/movie/movie-page.component.ts index 813a183a9..94e841b31 100644 --- a/packages/demo/src/app/pages/movie/movie-page.component.ts +++ b/packages/demo/src/app/pages/movie/movie-page.component.ts @@ -1,6 +1,6 @@ -import { Apollo, gql } from 'apollo-angular'; +import { Apollo, gql, onlyComplete } from 'apollo-angular'; import { Observable } from 'rxjs'; -import { filter, map } from 'rxjs/operators'; +import { map } from 'rxjs/operators'; import { AsyncPipe } from '@angular/common'; import { Component, inject, OnInit } from '@angular/core'; import { ActivatedRoute, RouterLink } from '@angular/router'; @@ -69,10 +69,11 @@ export class MoviePageComponent implements OnInit { variables: { id: this.route.snapshot.paramMap.get('id')!, }, + notifyOnNetworkStatusChange: false, }) .valueChanges.pipe( - map(result => (result.dataState === 'complete' ? result.data.film : null)), - filter(Boolean), + onlyComplete(), + map(result => result.data.film), ); } } diff --git a/packages/demo/src/app/pages/movies/movies-page.component.ts b/packages/demo/src/app/pages/movies/movies-page.component.ts index e7e3e3e9d..0ad79a82d 100644 --- a/packages/demo/src/app/pages/movies/movies-page.component.ts +++ b/packages/demo/src/app/pages/movies/movies-page.component.ts @@ -1,6 +1,6 @@ -import { Apollo, gql } from 'apollo-angular'; +import { Apollo, gql, onlyComplete } from 'apollo-angular'; import { Observable } from 'rxjs'; -import { filter, map } from 'rxjs/operators'; +import { map } from 'rxjs/operators'; import { AsyncPipe } from '@angular/common'; import { Component, inject, OnInit } from '@angular/core'; import { RouterLink } from '@angular/router'; @@ -56,10 +56,11 @@ export class MoviesPageComponent implements OnInit { } } `, + notifyOnNetworkStatusChange: false, }) .valueChanges.pipe( - map(result => (result.dataState == 'complete' ? result.data.allFilms.films : null)), - filter(Boolean), + onlyComplete(), + map(result => result.data.allFilms.films), ); } }