Skip to main content

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
PropertyTypeDescription
fetchPolicy?WatchQueryFetchPolicySpecifies 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?WatchQueryFetchPolicyDefaults 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?RefetchWritePolicySpecifies whether a NetworkStatus.refetch operation should merge
incoming 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?ErrorPolicySpecifies 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?DefaultContextIf you're using Apollo Link, this object is the initial value of the context object that's passed along your link chain.
pollInterval?numberSpecifies the interval (in milliseconds) at which the query polls for updated results.

The default value is 0 (no polling).
notifyOnNetworkStatusChange?booleanIf 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?booleanIf 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?() => booleanA callback function that's called whenever a refetch attempt occurs
while polling. If the function returns true, the refetch is
skipped and not reattempted until the next poll interval.
queryDocumentNode | TypedDocumentNode<TData, TVariables>A GraphQL query string parsed into an AST with the gql template literal.
notifyOnLoading?booleanWhether or not to track initial network loading status.
@default: true
lazy?booleanWhether to execute query immediately or lazily via execute method.
injector?InjectorCustom 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
SignalTypeDescription
resultSignal<QueryResult<TData, TStates>>The query result, containing data, loading, error, networkStatus, previousData, dataState.
loadingSignal<boolean>If true, the query is currently in flight.
networkStatusSignal<NetworkStatus>The current network status of the query.
dataSignal<GetData<TData, TStates> | undefined>The data returned by the query, or undefined if loading, errored, or no data received yet.
previousDataSignal<GetData<TData, TStates> | undefined>The data from the previous successful result, useful for displaying stale data during refetches.
errorSignal<ErrorLike | undefined>An error object if the query failed, undefined otherwise.
activeSignal<boolean>Whether the query is currently active, subscribed to the underlying observable and receiving cache updates.
enabledSignal<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
MethodDescription
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:

library/gql/book.graphql
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.

tip

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 a BookFragment then our watchQuery observable will automatically emit the new value when the mutation executes successfully.

Next, define the query in your component using signal.query:

books/books.component.ts
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:

library/books/books.component.html
<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.

library/books/books.component.ts
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:

library/book/book.component.ts
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 is enabled 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:

library/books/books.component.ts
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.

info

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 effects 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.

library/books.component.ts
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]
})
})));
}
}