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/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/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/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.
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 { ApolloQuery, customElement } from '@apollo-elements/lit-apollo';
import PostQuery from './Post.query.graphql';
import { routeVar } from '../variables';
@customElement('blog-post')
class BlogPost extends ApolloQuery<Data, Variables> {
query = PostQuery;
shouldSubscribe() {
return !!(routeVar().params?.postId)
}
}
import { ApolloQuery, customElement } from '@apollo-elements/fast';
import PostQuery from './Post.query.graphql';
import { routeVar } from '../variables';
@customElement({ name: 'blog-post' })
class BlogPost extends ApolloQuery<Data, Variables> {
query = 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 { define, query, client } from '@apollo-elements/hybrids';
import PostQuery from './Post.query.graphql';
import { routeVar } from '../variables';
function shouldSubscribe() {
return !!(routeVar().params?.postId)
}
define('blog-post', {
client: client(window.__APOLLO_CLIENT__),
query: query(PostQuery),
shouldSubscribe: { get(host) { return shouldSubscribe(host); } }
});
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
import { ValidateVariablesMixin, ApolloQueryMixin } from '@apollo-elements/mixins';
import NonNullableQuery from './NonNullable.query.graphql';
class NonNullable extends
ValidateVariablesMixin(ApolloQueryMixin(HTMLElement))<Data, Variables> {
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)<Data, Variables> {
query = NonNullableQuery;
}
import { ValidateVariablesMixin } from '@apollo-elements/mixins';
import { ApolloQuery, customElement } from '@apollo-elements/fast';
import NonNullableQuery from './NonNullable.query.graphql';
@customElement({ name: 'non-nullable' })
class NonNullable extends
ValidateVariablesMixin(ApolloQuery)<Data, Variables> {
query = NonNullableQuery;
}
> There's no `ValidateVariablesMixin` for haunted, so use one of the other techniques.
> There's no `ValidateVariablesMixin` for hybrids, so use one of the other techniques.