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:
- Create a variable-validating link
- Override the
shouldSubscribe
method on your components to determine whether they should subscribe - Opt in to the old behaviour with
ValidateVariablesMixin
Option 1: Create a Variable-Validating Link
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.