Apollo Elements Apollo Elements Guides API Blog Toggle darkmode

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/or variables properties or class fields. Render your component's local DOM with the component's data, loading, and error 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:

  1. Using the <apollo-query> HTML element
  2. With ApolloQueryController reactive controller; useQuery hook for haunted or atomico; or query hybrids factory
  3. 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:

  1. By setting the no-auto-subscribe attribute
  2. By overriding the shouldSubscribe method
  3. 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 and apollo-error events
  • call the executeQuery method and await 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.

Footnotes

  1. or applies ApolloQueryMixin
  2. This is different from GraphQL subscriptions, which are realtime persistent streams, typically over websockets. Rather, ObservableQueries update the client-side state whenever the query's data changes, either because the user executed the query operation, a mutation updated the query's fields, or the Apollo cache's local state changed.
  3. if you've previously set a 'cache-only' fetch policy and you want to imperatively issue query over the network, call executeQuery({ fetchPolicy: 'network-only' })