Apollo Elements Guides API Blog

Building Apps: Queries

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.

Here for the query component API docs?

GraphQL queries are how you read 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".

Query Elements

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 }
}

To initiate a query, set your element's query property or class field, or add an application/graphql script in your element's HTML.

document.querySelector('hello-query').query = HelloQuery;
<hello-query>
  <script type="application/graphql">
    query HelloQuery {
      hello { name greeting }
    }
  </script>
</hello-query>

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.

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 { ApolloQuery, customElement } from '@apollo-elements/lit-apollo';
import { HelloQuery } from './Hello.query.graphql';

@customElement('hello-query')
export class HelloQueryElement extends ApolloQuery<Data, Variables> {
  query = HelloQuery;

  render() {
    const greeting = this.data?.greeting ?? 'Hello';
    const name = this.data?.name ?? 'Friend';
    return html`
      <article class="${classMap({ skeleton: this.loading })}">
        <p id="error" ?hidden="${!this.error}">${this.error?.message}</p>
        <p>
          ${this.data?.greeting ?? 'Hello'},
          ${this.data?.name ?? 'Friend'}
        </p>
      </article>
    `;
  }
}
import { ApolloQuery, customElement } from '@apollo-elements/fast';
import HelloQuery from './Hello.query.graphql';

const template = html<HelloQueryElement>`
  <article class="${x => x.loading ? 'skeleton' : ''})}">
    <p id="error" ?hidden="${!x => x.error}">${x => x.error?.message}</p>
    <p>
      ${x => x.data?.hello?.greeting ?? 'Hello'},
      ${x => x.data?.hello?.name ?? 'Friend'}
    </p>
  </article>
`;

@customElement({ name: 'hello-query', template })
export class HelloQueryElement extends ApolloQuery<Data, Variables> {
  query = HelloQuery;
}
import { useQuery, component, html } from '@apollo-elements/haunted';
import { classMap } from 'lit-html/directives/class-map';
import { HelloQuery } from './Hello.query.graphql';

function Hello() {
  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 { 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'
};

If your variables are static, you can even do this by adding a JSON script to your element's HTML.

<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>
</apollo-query>

For class-based components (e.g. vanilla, lit-apollo, or FAST), you can apply arguments by setting the variables class field, while the useQuery haunted hook and query hybrids factory take a second options parameter with a variables property.

export class HelloQueryElement extends ApolloQueryMixin(HTMLElement)<Data, Variables> {
  query = HelloQuery;

  variables = {
    greeting: "How's it going",
    name: 'Dude'
  };
}
export class HelloQueryElement extends ApolloQuery<Data, Variables> {
  query = HelloQuery;

  variables = {
    greeting: "How's it going",
    name: 'Dude'
  };
}
@customElement({ name: 'hello-query', template })
export class HelloQueryElement extends ApolloQuery<Data, Variables> {
  query = HelloQuery;

  variables = {
    greeting: "How's it going",
    name: 'Dude'
  };
}
function Hello() {
  const { data } = useQuery(HelloQuery, {
    variables: {
      greeting: "How's it going",
      name: 'Dude'
    }
  });
}
define('hello-query', {
  client: client(),
  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.

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-element, 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 LitElement or dataChanged for FAST
  • 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

Preventing Automatic Subscription

If you want to keep your element from automatically subscribing, you can opt out of the default behaviour by setting the noAutoSubscribe property.

class LazyGreeting extends HelloQueryElement {
  noAutoSubscribe = true;
}
class LazyGreeting extends HelloQueryElement {
  noAutoSubscribe = true;
}
class LazyGreeting extends HelloQueryElement {
  noAutoSubscribe = true;
}
function Hello() {
  const { data } = useQuery(HelloQuery, { noAutoSubscribe: true });

  const greeting = data?.greeting ?? 'Hello';
  const name = data?.name ?? 'Friend';

  return html`
    <p>${greeting}, ${name}!</p>
  `;
}
define('lazy-hello-world', {
  noAutoSubscribe: true,
  client: client(),
  query: query(HelloQuery),
});

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.

NOTE, for hybrids, if you explicitly define a noAutoSubscribe property in the descriptor, this attribute may not set the associated property.

<!-- 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>

If you want your component to automatically subscribe, but only if its required variables are present, see Validating Variables.

Footnotes

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