signal.query
Create a reactive signal-based wrapper around a GraphQL query.
API
Apollo.signal.query<TData, TVariables>(
options: SignalQueryOptions<TVariables, TData>
): SignalQuery<TData, TVariables>
Returns a SignalQuery<TData, TVariables>
instance. This object provides several reactive Signals and methods for interaction.
Options
Property | Type | Description |
---|---|---|
fetchPolicy? | WatchQueryFetchPolicy | Specifies how the query interacts with the Apollo Client cache during execution (for example, whether it checks the cache for results before sending a request to the server). For details, see Setting a fetch policy. The default value is cache-first . |
initialFetchPolicy? | WatchQueryFetchPolicy | Defaults to the initial value of options.fetchPolicy, but can be explicitly configured to specify the WatchQueryFetchPolicy to revert back to whenever variables change (unless nextFetchPolicy intervenes). |
refetchWritePolicy? | RefetchWritePolicy | Specifies whether a NetworkStatus.refetch operation should mergeincoming field data with existing data, or overwrite the existing data. Overwriting is probably preferable, but merging is currently the default behavior, for backwards compatibility with Apollo Client 3.x. |
errorPolicy? | ErrorPolicy | Specifies how the query handles a response that returns both GraphQL errors and partial results. For details, see GraphQL error policies. The default value is all , meaning that the promise returned from execute method always resolves to a result with an optional error field. |
context? | DefaultContext | If you're using Apollo Link, this object is the initial value of the context object that's passed along your link chain. |
pollInterval? | number | Specifies the interval (in milliseconds) at which the query polls for updated results. The default value is 0 (no polling). |
notifyOnNetworkStatusChange? | boolean | If true , the in-progress query's associated component re-renders whenever the network status changes or a network error occurs.The default value is true . |
returnPartialData? | boolean | If true , the query can return partial results from the cache if the cache doesn't contain results for all queried fields.The default value is false . |
skipPollAttempt? | () => boolean | A callback function that's called whenever a refetch attempt occurs while polling. If the function returns true , the refetch isskipped and not reattempted until the next poll interval. |
query | DocumentNode | TypedDocumentNode<TData, TVariables> | A GraphQL query string parsed into an AST with the gql template literal. |
notifyOnLoading? | boolean | Whether or not to track initial network loading status. @default: true |
lazy? | boolean | Whether to execute query immediately or lazily via execute method. |
injector? | Injector | Custom injector to use for this query. |
variables? | () => TVariables | undefined | | A function or signal returning an object containing all of the GraphQL variables your query requires to execute. Each key in the object corresponds to a variable name, and that key's value corresponds to the variable value. When null is returned, the query will be terminated until a non-null value is returned again. |
Signals
Signal | Type | Description |
---|---|---|
result | Signal<QueryResult<TData, TStates>> | The query result, containing data , loading , error , networkStatus , previousData , dataState . |
loading | Signal<boolean> | If true , the query is currently in flight. |
networkStatus | Signal<NetworkStatus> | The current network status of the query. |
data | Signal<GetData<TData, TStates> | undefined> | The data returned by the query, or undefined if loading, errored, or no data received yet. |
previousData | Signal<GetData<TData, TStates> | undefined> | The data from the previous successful result, useful for displaying stale data during refetches. |
error | Signal<ErrorLike | undefined> | An error object if the query failed, undefined otherwise. |
active | Signal<boolean> | Whether the query is currently active, subscribed to the underlying observable and receiving cache updates. |
enabled | Signal<boolean> | Whether the query is currently enabled. This property starts as true for non-lazy queries and false for lazy queries.Calling execute() sets it to true , while calling terminate() sets it to false .When true :- The query automatically executes when variables change from null to a non-null value- Variable changes trigger re-execution with the new variables When false :- Variable changes are ignored and do not trigger re-execution - The query must be manually started via execute() Note: This is different from active , which indicates whether the query is currently connected to its observable and actively watching the cache. |
Methods
Method | Description |
---|---|
execute(execOptions: SignalQueryExecOptions<TVariables>) | Execute the query with the provided options. |
terminate() | Terminate query execution and unsubscribe from the observable. |
refetch(variables?: Partial<TVariables>) | Refetch the query with the current variables. |
fetchMore<TFetchData, TFetchVars>(options: FetchMoreOptions<TData, TVariables, TFetchData, TFetchVars>) | Fetch more data and merge it with the existing result. |
updateQuery(mapFn: UpdateQueryMapFn<TData, TVariables>) | Update the query's cached data. |
startPolling(pollInterval: number) | Start polling the query. |
stopPolling() | Stop polling the query. |
subscribeToMore<TSubscriptionData, TSubscriptionVariables>(options: SubscribeToMoreOptions<TData, TSubscriptionVariables, TSubscriptionData, TVariables>) | Subscribe to more data. |
Executing a query
To execute a query within an Angular component using Signals, inject Apollo
and call signal.query
with a GraphQL query document. The returned SignalQuery
object's signals can be used directly in your component's template or logic to reactively render the UI based on the query's state (loading
, error
, data
).
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 BOOKS_QUERY
typed gql document 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, define the query in your component using signal.query
:
import { Apollo } from '@apollo-orbit/angular';
import { BOOKS_QUERY } from '../graphql/types';
@Component({
selector: 'app-books',
templateUrl: './books.component.html'
})
export class BooksComponent {
private readonly apollo = inject(Apollo);
protected readonly booksQuery = this.apollo.signal.query({ query: BOOKS_QUERY });
}
Then, use the signals directly in the template to handle the different query states. Note the absence of the async
pipe:
<h3>Books</h3>
@if (booksQuery.loading()) { Loading... }
@if (booksQuery.error(); as error) { {{ error.message }} }
@for (book of booksQuery.data()?.books; track book.id) {
<div>{{ book.displayName }}</div>
}
Variables
A variables function or signal may be passed to signal.query
options.
If the function depends on reactive variables (e.g., based on component inputs or other signals), SignalQuery
will automatically update the underlying query variables when these reactive variables change.
import { Apollo } from '@apollo-orbit/angular';
import { BOOKS_QUERY } from '../graphql/types';
@Component({
selector: 'app-books',
templateUrl: './books.component.html'
})
export class BooksComponent {
private readonly apollo = inject(Apollo);
// Component input signal
public readonly authorId = input<string | undefined>();
// Component-defined signal
private readonly genre = signal<string | undefined>('Fiction');
protected readonly booksQuery = this.apollo.signal.query({
query: BOOKS_QUERY,
variables: () => ({
genre: this.genre(),
authorId: this.authorId()
})
});
// Derived reactive signal for the number of books
protected readonly booksCount = computed(() => this.booksQuery.data()?.books.length);
protected setGenre(newGenre?: string): void {
this.genre.set(newGenre);
}
}
This setup ensures that SignalQuery
automatically reacts to changes in genre
or the authorId
input signal, refetching data as needed.
Variables = null
When the variables function returns null
, SignalQuery
will automatically terminate execution:
import { Apollo } from '@apollo-orbit/angular';
import { BOOK_QUERY } from '../graphql/types';
@Component({
selector: 'app-book',
templateUrl: './book.component.html'
})
export class BookDetailsComponent {
private readonly apollo = inject(Apollo);
// Component input that might be null initially
public readonly bookId = input<string | null>();
protected readonly bookQuery = this.apollo.signal.query({
query: BOOK_QUERY,
variables: () => {
const id = this.bookId();
return id ? { id } : null; // Return null if bookId is not available
}
});
}
Behavior when variables return null
:
- If the query hasn't executed yet, it won't execute until variables become non-null
- If the query is already active, it will terminate (disconnect from the observable and stop watching cache updates)
- When variables change from
null
to a non-null value, if the query isenabled
then it will automatically execute
This pattern is useful for:
- Waiting for required input parameters before executing a query
- Conditionally executing queries based on user selections
- Preventing unnecessary queries when dependencies are not ready
Lazy Queries
In some cases, you may want to delay the execution of a query until a specific event occurs (e.g., a button click). This can be achieved by using the lazy
option in signal.query
:
import { Apollo } from '@apollo-orbit/angular';
import { BOOKS_QUERY } from '../graphql/types';
@Component({
selector: 'app-books',
templateUrl: './books.component.html'
})
export class BooksComponent {
private readonly apollo = inject(Apollo);
...
protected readonly lazyBooksQuery = this.apollo.signal.query({
query: BOOKS_QUERY,
lazy: true, // Delay execution until explicitly called
variables: () => ({
genre: this.genre(),
authorId: this.authorId()
})
});
protected async onBooksSectionExpanded(): Promise<void> {
// Execute the query. It will use the latest value from the variables function.
// This can be safely called multiple times even if the query is already active.
const { error, data } = await this.lazyBooksQuery.execute();
if (error) {
// Optionally handle error
} else if (data) {
// Optionally handle data
}
}
protected onBooksSectionCollapsed(): void {
// Terminate the query and stop listening to variables changes or cache updates.
this.lazyBooksQuery.terminate();
}
}
Alternatively, you can pass a variables
object to execute({ variables: { genre: 'Fiction' } })
to override the values provided in the signal.query
options for that specific execution. Keep in mind that any future changes in the variables
function (if provided in the initial options) will still automatically update the query variables after the initial execution.
Setting lazy: true
makes the query variables optional (even if required by the query), allowing you to pass them only when executing the query via the execute
method.
By default, signal.query
has errorPolicy
set to all
This ensures that the promise returned by execute()
always resolves without a rejection even if the query encounters errors. Removing the need for try...catch
or catching unhandled promise rejections.
Component Lifecycle
Understanding how SignalQuery
fits into the Angular component lifecycle is crucial for effective usage.
Similar to Angular's resource
/rxResource
, SignalQuery
utilises an effect
internally to establish a subscription to the underlying watchQuery
observable.
The timing of this effect is important. In Angular v19+, component effect
s are executed as part of the component's change detection lifecycle, after the component input bindings have been set and before component ngOnInit
lifecycle hook.
Consequently, the earliest safe place to call SignalQuery
instance methods like refetch
, fetchMore
, or subscribeToMore
, is within a separate component effect
defined in the constructor. This ensures your effect runs after the internal setup has occurred. Attempting to call these methods directly in the constructor or in ngOnInit
, before the underlying query observable has been instantiated, will throw a SignalQueryExecutionError
.
Similarly, calling these methods on a lazy
SignalQuery
instance before calling execute()
will also throw SignalQueryExecutionError
.
The following diagram shows where SignalQuery
typically fits within the Angular component lifecycle:
Example: subscribeToMore
Let's enhance the BooksComponent
to subscribe to real-time updates when a new book is added.
import { Apollo } from '@apollo-orbit/angular';
import { BOOKS_QUERY, NEW_BOOK_SUBSCRIPTION } from '../graphql/types';
@Component({
selector: 'app-books',
templateUrl: './books.component.html'
})
export class BooksComponent {
private readonly apollo = inject(Apollo);
protected readonly booksQuery = this.apollo.signal.query({ query: BOOKS_QUERY });
public constructor() {
effect(onCleanup => onCleanup(this.booksQuery.subscribeToMore({
subscription: NEW_BOOK_SUBSCRIPTION,
updateQuery: (prev, { subscriptionData }) => ({
...prev,
books: [...prev.books, subscriptionData.data.newBook]
})
})));
}
}