Apollo Elements Apollo Elements Guides API Blog Toggle darkmode

Cool Tricks: Validating Variables

Queries that have non-nullable variables (i.e. required variables) will still attempt to subscribe even if their required variables are not set.

For example, this query reads the "route param" /:pageId and exports it as the $pageId variable:

query Post($postId: ID!) {
  route @client {
    params {
      postId @export(as: "postId")
    }
  }

  post(postId: $postId) {
    id
    title
    image
    content
  }
}

Let's imagine a client-side router which calculates the /:pageId route param from the current page URL, and updates the routeVar reactive variable on every page navigation.

import { ApolloClient, HttpLink, InMemoryCache, makeVar } from '@apollo/client/core';

// router implementation left to reader's imagination
import { router, makeRoute } from 'foo-uter';

export const routeVar = makeVar(makeRoute(window.location))

router.addEventListener('navigate', location =>
  routeVar(makeRoute(location)));

const client = new ApolloClient({
  link: new HttpLink({ uri: '/graphql' }),
  cache: new InMemoryCache({
    typePolicies: {
      Query: {
        fields: {
          route() {
            return routeVar();
          }
        }
      }
    }
  })
});

If a component subscribes to this query automatically, as is the default behaviour, the graphql server may respond with an error:

Variable ”$postId“ of required type ”ID!“ was not provided.

For queries like this one, where the variables are dynamic, (e.g. they're based on the current page URL), you have three good options to prevent these kinds of errors:

  1. Create a variable-validating link
  2. Override the shouldSubscribe method on your components to determine whether they should subscribe
  3. Opt in to the old behaviour with ValidateVariablesMixin

To prevent any operation from fetching without required variables, use hasAllVariables from @apollo-elements/core/lib to create an ApolloLink which checks every outgoing operation for required variables.

import { ApolloClient, ApolloLink, HttpLink, InMemoryCache } from '@apollo/client/core';

import { hasAllVariables } from '@apollo-elements/core/lib/has-all-variables';

export const client = new ApolloClient({
  cache: new InMemoryCache(),
  link: ApolloLink.from([
    new ApolloLink((operation, forward) =>
      hasAllVariables(operation) && forward(operation)),
    new HttpLink({ uri: '/graphql' }),
  ])
});

The <apollo-client> component from @apollo-elements/components/apollo-client and the createApolloClient({ validateVariables: true }) helper from @apollo-elements/core/lib/create-apollo-client both incorporate this link.

This option is great when you want to 'set it and forget it', and it works for any operation, not solely for queries, but it's heavy-handed. For more fine-grained control you can program each individual query component to defer querying.

Option 2: Override the shouldSubscribe Method

With this approach, you can control on a per-component basis when to subscribe.

<p>
  You can subclass <code>ApolloQueryElement</code>
  to override methods.
</p>

<should-subscribe-query></should-subscribe-query>

<script type="module">
import { ApolloQueryElement } from '@apollo-elements/components/apollo-query';
import { routeVar } from '../variables';

customElements.define(
  'should-subscribe-query',
  class ShouldSubscribeQueryElement extends ApolloQueryElement {
    shouldSubscribe() {
      return !!(routeVar().params?.postId);
    }
  });
</script>
import { ApolloQueryMixin } from '@apollo-elements/mixins/apollo-query-mixin';
import { PostQuery } from './Post.query.graphql';
import { routeVar } from '../variables';

class BlogPost extends ApolloQueryMixin(HTMLElement)<Data, Variables> {
  query = PostQuery;

  shouldSubscribe() {
    return !!(routeVar().params?.postId)
  }
}

customElements.define('blog-post', BlogPost);
import { ApolloQueryController } from '@apollo-elements/core';
import { LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import { PostQuery } from './Post.query.graphql';
import { routeVar } from '../variables';

@customElement('blog-post')
class BlogPost extends LitElement {
  query = new ApolloQueryController(this, PostQuery, {
    shouldSubscribe() {
      return !!(routeVar().params?.postId)
    }
  });
}
import { FASTElement, customElement } from '@microsoft/fast-element';
import { ApolloQueryBehavior } from '@apollo-elements/fast';
import { PostQuery } from './Post.query.graphql';
import { routeVar } from '../variables';

@customElement({ name: 'blog-post' })
class BlogPost extends FASTElement {
  query = new ApolloQueryBehavior(this, PostQuery, {
    shouldSubscribe() {
      return !!(routeVar().params?.postId)
    }
  });
}
import { useQuery, component } from '@apollo-elements/haunted';
import { PostQuery } from './Post.query.graphql';
import { routeVar } from '../variables';

function BlogPost() {
  const { data } = useQuery(PostQuery, {
    shouldSubscribe() {
      return !!(routeVar().params?.postId)
    },
  });
}

customElements.define('blog-post', component(BlogPost));
import { useQuery, c } from '@apollo-elements/atomico';
import { PostQuery } from './Post.query.graphql';
import { routeVar } from '../variables';

function BlogPost() {
  const { data } = useQuery(PostQuery, {
    shouldSubscribe() {
      return !!(routeVar().params?.postId)
    },
  });
  return <host>...</host>;
}

customElements.define('blog-post', c(BlogPost));
import { define, query } from '@apollo-elements/hybrids';
import { PostQuery } from './Post.query.graphql';
import { routeVar } from '../variables';

function shouldSubscribe() {
  return !!(routeVar().params?.postId)
}

define('blog-post', {
  query: query(PostQuery, { shouldSubscribe }),
});

Option 3: Restore the Old Behaviour with ValidateVariablesMixin

The old variable-validating behaviour is still available, but you have to opt-in to get it. Import the ValidateVariablesMixin from @apollo-elements/mixins/validate-variables-mixin and apply it to your base class

<p>
  You can subclass <code>ApolloQueryElement</code>
  to apply mixins.
</p>

<validated-variables-query></validated-variables-query>

<script type="module">
import { ApolloQueryElement } from '@apollo-elements/components/apollo-query';
customElements.define(
  'validated-variables-query',
  class ValidatedApolloQuery extends ValidateVariablesMixin(ApolloQueryElement) {});
</script>
import { ValidateVariablesMixin, ApolloQueryMixin } from '@apollo-elements/mixins';

import { NonNullableQuery } from './NonNullable.query.graphql';

class NonNullable extends
ValidateVariablesMixin(ApolloQueryMixin(HTMLElement))<typeof NonNullableQuery> {
  query = NonNullableQuery;
}

customElements.define('non-nullable');
import { ValidateVariablesMixin } from '@apollo-elements/mixins';
import { ApolloQuery, customElement } from '@apollo-elements/lit-apollo';

import { NonNullableQuery } from './NonNullable.query.graphql';

@customElement('non-nullable')
class NonNullable extends ValidateVariablesMixin(ApolloQuery)<typeof NonNullableQuery> {
  query = NonNullableQuery;
}
import { ValidateVariablesMixin } from '@apollo-elements/mixins';
import { customElement } from '@microsoft/fast-element';
import { ApolloQuery } from '@apollo-elements/fast/bases/apollo-query';

import { NonNullableQuery } from './NonNullable.query.graphql';

@customElement({ name: 'non-nullable' })
class NonNullable extends ValidateVariablesMixin(ApolloQuery)<typeof NonNullableQuery> {
  query = NonNullableQuery;
}
> There's no `ValidateVariablesMixin` for haunted, so use one of the other techniques.
> There's no `ValidateVariablesMixin` for atomico, so use one of the other techniques.
> There's no `ValidateVariablesMixin` for hybrids, so use one of the other techniques.