Usage: Queries
Query components combine a GraphQL query with a custom element, which you would usually define with a DOM template and optionally some custom JavaScript behaviours. In other words, each query component encapsulates GraphQL data (the query) and HTML/CSS/JS UI (the custom element).
This page is a HOW-TO guide. For detailed docs on the ApolloQuery
interface see the API docs
Queries are how your application reads data from the graph. You can think of them as roughly analogous to HTTP GET
requests or SQL READ
statements. A query can have variables, e.g. "user, by user ID" or "posts, sorted by last modified"; or it might not, e.g. "all users".
By default, query components automatically fetch their data over the network once they are added to the page, although you can configure the fetching behaviour in a handful of different ways. Once a query components starts fetching its data, it subscribes to all future updates; so conceptually, think of your component as rendering a DOM template using the latest, up-to-date result of its query.
TL;DR: query components read data from the graph. By default, query elements automatically fetch data once you set their
query
and/orvariables
properties or class fields. Render your component's local DOM with the component'sdata
,loading
, anderror
properties.
Query components read data from the GraphQL and expose them on the element's data
property. Each query element has a query
property which is a GraphQL DocumentNode
. You can create that object using the gql
template literal tag, or via @apollo-elements/rollup-plugin-graphql
, etc. See the buildless development guide for more info.
query HelloQuery {
hello { name greeting }
}
Apollo Elements give you three ways to define query components:
- Using the
<apollo-query>
HTML element - With
ApolloQueryController
reactive controller;useQuery
hook for haunted or atomico; orquery
hybrids factory - By defining a custom element that extends from
HTML Queries
The <apollo-query>
element from @apollo-elements/components
lets you declaratively create query components using HTML. It renders its template to its shadow root, so you get all the benefits of Shadow DOM encapsulation (you can opt out of Shadow DOM by setting the no-shadow
attribute). If your query's variables are static, adding a JSON script as a child of the element to initialize them and start the query.
<apollo-query>
<script type="application/graphql">
query HelloQuery($name: String, $greeting: String) {
helloWorld(name: $name, greeting: $greeting) {
name
greeting
}
}
</script>
<script type="application/json">
{
"greeting": "How's it going",
"name": "Dude"
}
</script>
<template>
<style>
#greeting { font-weight: bold; }
#name { font-style: italic; }
</style>
<span id="greeting">{{ data.helloWorld.greeting }}</span>,
<span id="greeting">{{ data.helloWorld.name }}</span>,
</template>
</apollo-query>
Read more about <apollo-query>
in the <apollo-query>
HTML element guide.
Custom Query Elements
Apollo Elements gives you multiple options for defining your own custom query elements. Which option you choose depends on your application, your team, and your priorities. You can extend from the lit or FAST base classes, or apply the Apollo query mixin to the base class of your choice. For those who prefer a more 'functional' approach, there's the useQuery
haunted hook or the query
hybrids factory.
In any case, setting your element's query property or class field (or using useQuery
or query
factory) will automatically start the subscription. You can change the query via the query
DOM property at any time to reinitialize the subscription.
document.querySelector('hello-query').query = HelloQuery;
Apollo client ensures that the component always has the latest data by subscribing to the query using an ObservableQuery
object. As long as an element has access to an ApolloClient
instance, whenever its query
or variables
property changes, it will automatically subscribe to (or update) its ObservableQuery
.
When the ObservableQuery
subscription produces new data (e.g. on response from the GraphQL server, or if local state changes), it sets the element's data
, loading
and error
properties (as well as errors
if returnPartialData
property is true). The following example shows how a simple query element written with different component libraries (or none) renders it's state.
<apollo-query>
<script type="application/graphql">
query HelloQuery {
hello { name greeting }
}
</script>
<template>
<article class="{{ loading ? 'skeleton' : '' }}">
<p id="error" ?hidden="{{ !error }}">{{ error.message }}</p>
<p>
{{ data.greeting || 'Hello' }},
{{ data.name || 'Friend' }}
</p>
</article>
</template>
</apollo-query>
import { ApolloQueryMixins } from '@apollo-elements/mixins/apollo-query-mixin';
import HelloQuery from './Hello.query.graphql';
const template = document.createElement('template');
template.innerHTML = `
<article class="skeleton">
<p id="error" hidden></p>
<p id="data"></p>
</article>
`;
template.content.querySelector('#data').append(new Text('Hello'));
template.content.querySelector('#data').append(new Text(', '));
template.content.querySelector('#data').append(new Text('Friend'));
template.content.querySelector('#data').append(new Text('!'));
export class HelloQueryElement extends
ApolloQueryMixin(HTMLElement)<Data, Variables> {
query = HelloQuery;
constructor() {
super();
this.attachShadow({ mode: 'open' })
.append(template.content.cloneNode(true));
this.render();
}
$(selector) { return this.shadowRoot.querySelector(selector); }
#data: Data = null;
get data() { return this.#data; }
set data(value: Data) { this.#data = value; this.render(); }
#loading = false;
get loading() { return this.#loading; }
set loading(value: boolean) { this.#loading = value; this.render(); }
#error: Error | ApolloError = null;
get error() { return this.#error; }
set error(value: ApolloError) { this.#error = value; this.render(); }
render() {
if (this.loading)
this.$('article').classList.add('skeleton');
else
this.$('article').classList.remove('skeleton');
if (this.error) {
this.$('#error').hidden = false;
this.$('#error').textContent = this.error.message;
} else {
this.$('#error').hidden = true;
const [greetingNode, , nameNode] = this.$('#data').childNodes;
greetingNode.data = this.data?.hello?.greeting ?? 'Hello';
nameNode.data = this.data?.hello?.name ?? 'Friend';
}
}
}
customElements.define('hello-query', HelloQueryElement);
import { ApolloQueryController } from '@apollo-elements/core';
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
import { HelloQuery } from './Hello.query.graphql';
@customElement('hello-query')
export class HelloQueryElement extends LitElement {
query = new ApolloQueryController(this, HelloQuery);
render() {
const greeting = this.query.data?.greeting ?? 'Hello';
const name = this.query.data?.name ?? 'Friend';
return html`
<article class=${classMap({ skeleton: this.query.loading })}>
<p id="error" ?hidden=${!this.query.error}>${this.query.error?.message}</p>
<p>
${this.query.data?.greeting ?? 'Hello'},
${this.query.data?.name ?? 'Friend'}
</p>
</article>
`;
}
}
import { FASTElement, customElement, ViewTemplate } from '@microsoft/fast-element';
import { ApolloQueryBehavior } from '@apollo-elements/fast';
import { HelloQuery } from './Hello.query.graphql';
const template: ViewTemplate<HelloQueryElement> = html`
<article class=${x => x.query.loading ? 'skeleton' : ''}>
<p id="error" ?hidden=${!x => x.query.error}>${x => x.query.error?.message}</p>
<p>
${x => x.query.data?.hello?.greeting ?? 'Hello'},
${x => x.query.data?.hello?.name ?? 'Friend'}
</p>
</article>
`;
@customElement({ name: 'hello-query', template })
export class HelloQueryElement extends FASTElement {
query = new ApolloQueryBehavior(this, HelloQuery);
}
import { useQuery, component, html } from '@apollo-elements/haunted';
import { classMap } from 'lit/directives/class-map.js';
import { HelloQuery } from './Hello.query.graphql';
function HelloQueryElement() {
const { data, loading, error } = useQuery(HelloQuery, { noAutoSubscribe: true });
const greeting = data?.hello?.greeting ?? 'Hello';
const name = data?.hello?.name ?? 'Friend';
return html`
<article class=${classMap({ loading })}>
<p id="error" ?hidden=${!error}>${error?.message}</p>
<p>${greeting}, ${name}!</p>
</article>
`;
}
customElements.define('hello-query', component(Hello));
import { useQuery, c } from '@apollo-elements/atomico';
import { HelloQuery } from './Hello.query.graphql';
function HelloQueryElement() {
const { data, loading, error } = useQuery(HelloQuery, { noAutoSubscribe: true });
const greeting = data?.hello?.greeting ?? 'Hello';
const name = data?.hello?.name ?? 'Friend';
return (
<host shadowDom>
<article class={loading ? 'loading' : ''}>
<p id="error" hidden={!error}>{error?.message}</p>
<p>{greeting}, {name}!</p>
</article>
</host>
);
}
customElements.define('hello-query', c(Hello));
import { client, query, define, html } from '@apollo-elements/hybrids';
import { HelloQuery } from './Hello.query.graphql';
define('hello-query', {
client: client(),
query: query(HelloQuery),
render: ({ data, error, loading }) => html`
<article class=${loading ? 'skeleton' : ''}>
<p id="error" hidden=${!error}>${error?.message}</p>
<p>
${data?.hello?.greeting ?? 'Hello'},
${data?.hello?.name ?? 'Friend'}
</p>
</article>
`,
});
Query Variables
Some queries have variables, which you can use to customize the response from the GraphQL server:
query HelloQuery($name: String, $greeting: String) {
helloWorld(name: $name, greeting: $greeting) {
name
greeting
}
}
To apply variables to your query element, set its variables
property. For the above example, which has two string parameters, name
and greeting
, set your element's variables
property to an object with keys name
and greeting
and values representing the query arguments:
root.querySelector('hello-query').variables = {
greeting: "How's it going",
name: 'Dude'
};
For class-based components (e.g. vanilla, lit-apollo
, or FAST
), you can apply arguments by setting the variables
class field, while the ApolloQueryController
, useQuery
haunted hook and query
hybrids factory take a second options parameter with a variables
property.
<apollo-query>
<script type="application/graphql">
query HelloQuery {
hello { name greeting }
}
</script>
<script type="application/json">
{
"greeting": "How's it going",
"name": "Dude"
}
</script>
<template>
<article class="{{ loading ? 'skeleton' : '' }}">
<p id="error" ?hidden="{{ !error }}">{{ error.message }}</p>
<p>
{{ data.greeting || 'Hello' }},
{{ data.name || 'Friend' }}
</p>
</article>
</template>
</apollo-query>
export class HelloQueryElement extends ApolloQueryMixin(HTMLElement)<Data, Variables> {
query = HelloQuery;
variables = {
greeting: "How's it going",
name: 'Dude'
};
}
export class HelloQueryElement extends LitElement {
query = new ApolloQueryController(this, HelloQuery, {
variables: {
greeting: "How's it going",
name: 'Dude'
},
});
}
@customElement({ name: 'hello-query', template })
export class HelloQueryElement extends FASTElement {
query = new ApolloQueryBehavior(this, HelloQuery, {
variables: {
greeting: "How's it going",
name: 'Dude'
},
});
}
function Hello() {
const { data } = useQuery(HelloQuery, {
variables: {
greeting: "How's it going",
name: 'Dude'
}
});
}
function Hello() {
const { data } = useQuery(HelloQuery, {
variables: {
greeting: "How's it going",
name: 'Dude'
}
});
return <host>...</host>;
}
define('hello-query', {
query: query(HelloQuery, {
variables: {
greeting: "How's it going",
name: 'Dude'
}
}),
});
Variables can be non-nullable i.e. required. To prevent your element from fetching until it has all it's required variables, see validating variables.
Configuring Fetching
There are three main ways to control how and when your query component fetches its data:
- By setting the
no-auto-subscribe
attribute - By overriding the
shouldSubscribe
method - By setting a custom
FetchPolicy
You can call your component's executeQuery()
method at any time to immediately fetch the query.
No Auto Subscribe
If you want to keep your element from automatically subscribing, you can opt out of the default behaviour by setting the noAutoSubscribe
DOM property.
<apollo-query no-auto-subscribe>
<script type="application/graphql">...</script>
<template>...</template>
</apollo-query>
class LazyGreeting extends HelloQueryElement {
noAutoSubscribe = true;
}
export class HelloQueryElement extends LitElement {
query = new ApolloQueryController(this, HelloQuery, {
noAutoSubscribe: true,
variables: {
greeting: "How's it going",
name: 'Dude'
},
});
}
class LazyGreeting extends FASTElement {
query = new ApolloQueryController(this, HelloQuery, {
noAutoSubscribe: true,
variables: {
greeting: "How's it going",
name: 'Dude'
},
});
}
function Hello() {
const { data } = useQuery(HelloQuery, { noAutoSubscribe: true });
const greeting = data?.greeting ?? 'Hello';
const name = data?.name ?? 'Friend';
return html`
<p>${greeting}, ${name}!</p>
`;
}
function Hello() {
const { data } = useQuery(HelloQuery, { noAutoSubscribe: true });
const greeting = data?.greeting ?? 'Hello';
const name = data?.name ?? 'Friend';
return <host><p>{greeting}, {name}!</p></host>;
}
define('lazy-hello-world', {
query: query(HelloQuery, { noAutoSubscribe: true }),
});
Once you do, the element won't fetch any data unless you call its subscribe()
or executeQuery()
methods.
const element = document.querySelector('hello-query')
element.subscribe();
You can also set the boolean no-auto-subscribe
attribute to the element instance. Bear in mind that no-auto-subscribe
is a boolean attribute, so it's presence indicates truthiness, and its absence indicates falsiness.
<!-- This one subscribes immediately -->
<hello-query></hello-query>
<!-- This one will not subscribe until called -->
<hello-query no-auto-subscribe></hello-query>
<!-- Neither will this one -->
<hello-query no-auto-subscribe="false"></hello-query>
NOTE, the no-auto-subscribe
attribute comes built-in for query class elements e.g. @apollo-elements/mixins/apollo-query-mixin.js
for controllers, hybrids components or haunted useQuery
hooks, you can pass the noAutoSubscribe
option to the controller, but you'll be in charge of reading the attribute yourself.
Overriding shouldSubscribe
The query component class' protected shouldSubscribe
method controls whether or not to subscribe to updates. The default implementation constantly returns true
. If you wish to customize that behaviour, override the method with your own custom predicate, like this example which checks for the presence of a query param in the page URL:
<no-auto-fetch-query>...</no-auto-fetch-query>
<script type="module">
import { ApolloQueryElement } from '@apollo-elements/components';
class NoAutoFetchQuery extends ApolloQueryElement {
/**
* Prevent fetching if the URL contains a `?noAutoFetch` query param
*/
shouldSubscribe() {
const { searchParams } = new URL(location.href);
return !searchParams.has('noAutoFetch');
}
}
customElements.define('no-auto-fetch-query', NoAutoFetchQuery);
</script>
class PageQueryElement extends ApolloQueryMixin(HTMLElement)<typeof PageQuery> {
query = PageQuery;
/**
* Prevent fetching if the URL contains a `?noAutoFetch` query param
*/
override shouldSubscribe(): boolean {
const { searchParams } = new URL(location.href);
return !searchParams.has('noAutoFetch');
}
}
export class HelloQueryElement extends LitElement {
query = new ApolloQueryController(this, HelloQuery, {
variables: {
greeting: "How's it going",
name: 'Dude'
},
/**
* Prevent fetching if the URL contains a `?noAutoFetch` query param
*/
shouldSubscribe(): boolean {
const { searchParams } = new URL(location.href);
return !searchParams.has('noAutoFetch');
},
});
}
class PageQueryElement extends FASTElement {
query = new ApolloQueryController(this, HelloQuery, {
variables: {
greeting: "How's it going",
name: 'Dude'
},
/**
* Prevent fetching if the URL contains a `?noAutoFetch` query param
*/
shouldSubscribe(): boolean {
const { searchParams } = new URL(location.href);
return !searchParams.has('noAutoFetch');
},
});
}
function PageQueryElement() {
const { data } = useQuery(PageQuery, {
/**
* Prevent fetching if the URL contains a `?noAutoFetch` query param
*/
shouldSubscribe(): boolean {
const { searchParams } = new URL(location.href);
return !searchParams.has('noAutoFetch');
}
});
}
function PageQueryElement() {
const { data } = useQuery(PageQuery, {
/**
* Prevent fetching if the URL contains a `?noAutoFetch` query param
*/
shouldSubscribe(): boolean {
const { searchParams } = new URL(location.href);
return !searchParams.has('noAutoFetch');
}
});
return <host>...</host>
}
define('page-query', {
query: query(PageQuery, {
/**
* Prevent fetching if the URL contains a `?noAutoFetch` query param
*/
shouldSubscribe() {
const { searchParams } = new URL(location.href);
return !searchParams.has('noAutoFetch');
},
}),
});
Setting a FetchPolicy
Fetch Policies are how Apollo client internally manages query behaviour. The default fetch policy for queries is cache-first
meaning that Apollo client will first check to see if a given operation (i.e. query-variables pair) already has complete data in the cache. If so, it will not fetch over the network. Set the fetchPolicy
property on your component to configure.
<apollo-query fetch-policy="cache-only">
<script type="application/graphql">...</script>
<template>...</template>
</apollo-query>
import type { FetchPolicy } from '@apollo/client/core';
class CacheOnlyQueryElement extends ApolloQueryMixin(HTMLElement)<typeof HeavySlowQuery> {
query = HeavySlowQuery;
fetchPolicy: FetchPolicy = 'cache-only';
}
export class HeavySlowQueryElement extends LitElement {
query = new ApolloQueryController(this, HeavySlowQuery, {
fetchPolicy: 'cache-only',
});
}
class HeavySlowQueryElement extends FASTElement {
query = new ApolloQueryBehavior(this, HeavySlowQuery, {
fetchPolicy: 'cache-only',
});
}
function HeavySlowQueryElement() {
const { data } = useQuery(HeavySlowQuery, {
fetchPolicy: 'cache-only',
});
}
function HeavySlowQueryElement() {
const { data } = useQuery(HeavySlowQuery, {
fetchPolicy: 'cache-only',
});
return <host>...</host>;
}
define('heavy-slow-query', {
query: query(HeavySlowQuery, {
fetchPolicy: 'cache-only',
}),
});
You can also use the fetch-policy
attribute on individual elements (if they implement the ApolloElement interface, e.g. <apollo-query>
or elements with ApolloQueryMixin
):
<apollo-query fetch-policy="network-only">
<script type="application/graphql">
query AlwaysFresh {
messages(sort: desc) {
id message
}
}
</script>
<template>
<h2>Latest Message:</h2>
<template type="if" if="{{ data }}">
<p>{{ data.messages[0].message }}</p>
</template>
</template>
</apollo-query>
If you want your query to fire once over the network, but subsequently to only use the client-side cache, use the nextFetchPolicy
property.
If you want your component to automatically subscribe, but only if its required variables are present, see Validating Variables.
Reacting to Updates
As we've seen query elements set their data
property whenever the query resolves. For vanilla components, you should define a data setter that renders your DOM, and for each library (lit
, FAST
, hybrids
, etc.), their differing reactivity systems ensure that your element renders when the data changes.
If you want to run other side effects, here are some options:
- use your library's reactivity system, e.g.
updated
for Lit - define
onData
callback - listen for the
apollo-query-result
andapollo-error
events - call the
executeQuery
method andawait
it's result.
For more information, see query element lifecycle
Next Steps
Read about the <apollo-query>
HTML element,
dive into the ApolloQuery
API and component lifecycle
or continue on to the mutations guide.