Queries
Prerequisites
This article assumes you're familiar with building basic GraphQL queries. If you need a refresher, we recommend this guide.
This article also assumes that you've already set up your environment as per getting started guide.
Overview
query
and watchQuery
are the two primary methods for fetching data from GraphQL back-end in Apollo Orbit, with slightly different behaviours.
For a full documentation of queries, please refer to the Apollo Client docs
query
query<TData, TVariables>(options: QueryOptions<TVariables, TData>): Observable<QueryResult<TData>>;
Returns a standard RxJS Observable<QueryResult<TData>>
that terminates after data is fetched.
By default, query
does not emit the query's initial loading status (notifyOnLoading = false
) and on failure it emits errors on the observable's error stream (throwError = true
).
It is ideal for once off queries, like inside an Angular route guard or resolver.
watchQuery
watchQuery<TData, TVariables>(options: WatchQueryOptions<TVariables, TData>): QueryObservable<TData, TVariables>
Returns a QueryObservable<TData, TVariables>
which extends RxJS's standard Observable<QueryResult<TData>>
and is a wrapper around Apollo Client's underlying ObservableQuery<TData, TVariables>
.
This allows direct calls to Observable
members like subscribe
and pipe
and also ObservableQuery
members like variables
, refetch
, fetchMore
and subscribeToMore
...
Unlike query
, watchQuery
's observable does not terminate once data is fetched, but instead, it'll continue watching the cache for any changes to fetched data.
By default, watchQuery
emits the query's initial loading status (notifyOnLoading = true
) and on failure it emits errors on the observable's next stream as part of QueryResult
's error
property (throwError = false
).
It is ideal for reactive components.
Executing a query
To execute a query within an Angular component, inject Apollo
and pass it a GraphQL query document. When your component renders, watchQuery
observable emits a QueryResult
object that contains loading
, error
, data
and previousData
properties you can use to render your UI.
Let's look at an example.
First, we'll create a GraphQL query named Books
:
fragment BookFragment on Book {
id
name
genre
authorId
}
query Books($name: String, $genre: String, $authorId: ID) {
books(name: $name, genre: $genre, authorId: $authorId) {
...BookFragment
}
}
Saving book.graphql will trigger codegen of BooksQuery
class in /graphql/types.ts file as per our setup.
For more information about codegen and how it works, please refer to the Codegen section.
It is recommended to define re-usable fragments, like BookFragment
above, for a couple of reasons:
- Codegen will generate a
BookFragment
type which can be consistently referenced throughout the codebase. - Using the same fragment in queries and mutations ensures that Apollo Client can normalize data in cache correctly. For example, if an
updateBook
mutation returns aBookFragment
then ourwatchQuery
observable will automatically emit the new value when the mutation executes successfully.
Next, we define the query in our component:
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Apollo } from '@apollo-orbit/angular';
import { BooksQuery } from '../graphql/types';
@Component({
selector: 'app-books',
templateUrl: './books.component.html',
styleUrls: ['./books.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BooksComponent {
protected readonly booksQuery = this.apollo.watchQuery(new BooksQuery());
public constructor(
private readonly apollo: Apollo
) { }
}
Then, we subscribe to the query using async
pipe and handle the different query states:
<h3>Books</h3>
@if (booksQuery | async; as booksResult) {
@if (booksResult.loading) { Loading... }
@if (booksResult.error) { {{ booksResult.error.message }} }
@if (booksResult.data?.books; as books) {
@for (book of books; track book.id) {
<div>{{ book.name }}</div>
}
}
}
Variables
Variables are passed as constructor parameters to new BooksQuery()
:
@Component({
...
})
export class BooksComponent {
protected readonly booksQuery = this.apollo.watchQuery(new BooksQuery({ genre: 'Fiction' }));
...
}
This ensures that a query with required variables generates a class with required constructor parameters as explained in the Codegen section.
Options
For the complete list of options available to query methods, please refer to Apollo Client docs
In the next example, let's see how we can modify the query from the previous example in order to allow users to manually refetch the data.
First, we will need to pass notifyOnNetworkStatusChange
option to our watchQuery
method, this tells Apollo Client to set loading
property to true whenever a refetch is in flight.
In order to do that, we will need to spread the query
, variables
& context
properties of new BooksQuery()
and add notifyOnNetworkStatusChange: true
to the options object.
Secondly, we define a method in our component that calls refetch
method on the QueryObservable
instance.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Apollo } from '@apollo-orbit/angular';
import { BooksQuery } from '../graphql/types';
@Component({
selector: 'app-books',
templateUrl: './books.component.html',
styleUrls: ['./books.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BooksComponent {
protected readonly booksQuery = this.apollo.watchQuery(new BooksQuery());
protected readonly booksQuery = this.apollo.watchQuery({ ...new BooksQuery(), notifyOnNetworkStatusChange: true });
public constructor(
private readonly apollo: Apollo
) { }
protected refetch(): void {
this.booksQuery.refetch();
}
}
Then, we modify the template to include a button that calls refetch
, and finally, we replace booksResult.data?.books
with booksResult.data ?? booksResult.previousData
.
This is because while a refetch is in flight, the QueryResult
object will have { loading: true, data: undefined, ... }
which can provide a jarring user experience as the data disappears off the screen until the new data arrives.
Apollo Orbit exposes a previousData
property which stores the last non-nil value of the data
property that can be used to provide a smoother user experience.
<h3>
Books
<button type="button" (click)="refetch()">⟳</button>
</h3>
@if (booksQuery | async; as booksResult) {
@if (booksResult.loading) { Loading... }
@if (booksResult.error) { {{ booksResult.error.message }} }
@if (booksResult.data?.books; as books) {
@if ((booksResult.data ?? booksResult.previousData)?.books; as books) {
@for (book of books; track book.id) {
<div>{{ book.name }}</div>
}
}
}
mapQuery
Apollo Orbit provides a mapQuery
RxJS operator to map both data
and previousData
of a query result while preserving the other properties.
This is useful when you want to map the query result without dealing with the nullability of data
and previousData
properties.
import { mapQuery } from '@apollo-orbit/angular';
...
protected readonly bookNames$: Observable<QueryResult<Array<string>>> = this.apollo.watchQuery(new BooksQuery()).pipe(
mapQuery(data => data.books.map(book => book.name))
);
cache.watchQuery
Apollo.cache.watchQuery
method can be used to watch data directly in the cache.
Unlike Apollo.watchQuery
, it can only query the cache and does not execute a network request or any of Apollo Link's middleware.
It is ideal when querying local-only data.
Example
protected readonly theme$ = this.apollo.cache.watchQuery(new ThemeQuery()).pipe(
map(({ data }) => data.theme)
);
Comparison
Apollo.cache.watchQuery
has few pros and cons compared to Apollo.watchQuery
method.
Pros
- By default
data
property returned by the observable is defined and does not require null-checking.- Unless
returnPartialData
option is set totrue
.
- Unless
- There's no need to handle loading and error states.
- Synchronous observable execution
- The data is returned instantly when the observable is subscribed to.
- When the observable is subscribed to from an Angular template, the template will complete rendering in a single cycle.
- View children referenced in the component will be available in
ngAfterViewInit
lifecycle hook. - The observable can be converted to a non-nil
Signal
usingtoSignal
function with{ requireSync: true }
option.
Cons
- Does not execute a network request if the data is not available in the cache.
- Throws an error if the any selected field in the query is not available in the cache.
- Unless
returnPartialData
option is set totrue
.
- Unless
Cyclic cache updates
Because of the synchronous nature of Apollo.cache.watchQuery
, attempting to update the cache in the observable's subscribe callback will cause Apollo Client to throw an already computing
error.
This can be avoided by piping the observable through observeOn(asyncScheduler)
which will queue the observer.next
call after cache update operation is complete, mimicking the behaviour of Apollo.watchQuery
.
this.apollo.cache.watchQuery(new ThemeQuery()).pipe(
observeOn(asyncScheduler),
).subscribe(({ data }) => {
// Update cache
});