In addition to queries and mutations, GraphQL supports a third operation type: subscriptions
.
Subscriptions enable you to fetch data for long-lasting operations that can change their result over time. They maintain an active connection to your GraphQL server via WebSocket, allowing the server to push updates to the subscription's result.
Subscriptions help notify your client in real-time about changes to back-end data, such as adding new objects, updated fields, and so on.
As subscriptions usually maintain a persistent connection, they shouldn't use the default HTTP transport that Apollo Client uses for queries and mutations. Instead, Apollo Client subscriptions most commonly communicate over WebSocket, via the community-maintained subscriptions-transport-ws library.
yarn add -D subscriptions-transport-ws
npm install --save-dev subscriptions-transport-ws
import { setClient } from 'glimmer-apollo';
import {
ApolloClient,
InMemoryCache,
createHttpLink,
split
} from '@apollo/client/core';
import { getMainDefinition } from '@apollo/client/utilities';
export default function setupApolloClient(context: object): void {
// WebSocket connection to the API
const wsLink = new WebSocketLink({
uri: 'ws://localhost:3000/subscriptions',
options: {
reconnect: true
}
});
// HTTP connection to the API
const httpLink = createHttpLink({
uri: 'http://localhost:3000/graphql'
});
// Cache implementation
const cache = new InMemoryCache();
// Split HTTP link and WebSockete link
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink
);
// Create the apollo client
const apolloClient = new ApolloClient({
link: splitLink,
cache
});
// Set default apollo client for Glimmer Apollo
setClient(context, apolloClient);
}
Let's define our GraphQL Subscription document.
import { gql } from 'glimmer-apollo';
export const ON_MESSAGED_ADDED = gql`
subscription OnMessageAdded($channel: String!) {
messageAdded(channel: $channel) {
id
message
}
}
`;
export type OnMessageAddedSubscriptionVariables = Exact<{
channel: string;
}>;
export type OnMessageAddedSubscription = {
__typename?: 'Subscription';
messageAdded?: {
__typename?: 'Message';
id: string;
message: string;
} | null;
};
Similar to useQuery
and useMutation
, useSubscription
is a utility function to create a Subscription Resource.
import { useSubscription } from 'glimmer-apollo';
import {
ON_MESSAGED_ADDED,
OnMessageAddedSubscription,
OnMessageAddedSubscriptionVariables
} from './subscriptions';
export default class LatestMessage extends Component {
latestMessage = useSubscription<
OnMessageAddedSubscription,
OnMessageAddedSubscriptionVariables
>(
this,
() => [
ON_MESSAGED_ADDED,
{
/* options */
}
]
);
}
this
is to keep track of destruction. When the context object (this
) is destroyed, all the subscriptions resources attached to it can be destroyed and the connection closed.useSubscription
should always be a function that returns an array.import { useSubscription } from 'glimmer-apollo';
import {
ON_MESSAGED_ADDED,
OnMessageAddedSubscription,
OnMessageAddedSubscriptionVariables
} from './subscriptions';
export default class LatestMessage extends Component {
latestMessage = useSubscription<
OnMessageAddedSubscription,
OnMessageAddedSubscriptionVariables
>(
this,
() => [
ON_MESSAGED_ADDED,
{
variables: {
channel: 'glimmer-apollo'
}
}
]
);
static template = hbs`
{{#if this.latestMessage.loading}}
Connecting..
{{else if this.latestMessage.error}}
Error!: {{this.latestMessage.error.message}}
{{else}}
<div>
New Message: {{this.latestMessage.data.messageAdded.message}}
</div>
{{/if}}
`;
}
You can pass a variables object as part of the options argument for useSubscription
args thunk.
latestMessage = useSubscription(this, () => [
ON_MESSAGED_ADDED,
{ variables: { channel: this.args.channel } }
]);
If your variables are tracked
, Glimmer Apollo will re-execute your subscription.
Alongside variables, you can pass additional options to useSubscription
. These options vary from fetch policies, error policies, and more.
latestMessage = useSubscription(this, () => [
ON_MESSAGED_ADDED,
{
variables: { channel: this.args.channel },
errorPolicy: 'all',
fetchPolicy: 'network-only',
ssr: false
}
]);
ssr
Glimmer Apollo supports SSR with FastBoot by default. Any subscriptions that are triggered while rendering in FastBoot are automatically awaited for the first message to respond.
The ssr
option allows disabling execution of subscriptions when running in SSR with FastBoot. It will skip the execution entirely in FastBoot but will execute when running in the Browser. This feature is useful if you are fetching secondary data to the page and can wait to be fetched.
clientId
This option specifies which Apollo Client should be used for the given subscription. Glimmer Apollo supports defining multiple Apollo Clients that are distinguished by a custom identifier while setting the client to Glimmer Apollo.
// ....
setClient(
this,
new ApolloClient({
/* ... */
}),
'my-custom-client'
);
// ....
latestMessages = useSubscription(this, () => [ON_MESSAGED_ADDED, { clientId: 'my-custom-client' }]);
loading
This is a handy property that allows us to inform our interface that we are loading data.
import { useSubscription } from 'glimmer-apollo';
import {
ON_MESSAGED_ADDED,
OnMessageAddedSubscription,
OnMessageAddedSubscriptionVariables
} from './subscriptions';
export default class LatestMessage extends Component {
latestMessage = useSubscription<
OnMessageAddedSubscription,
OnMessageAddedSubscriptionVariables
>(
this,
() => [
ON_MESSAGED_ADDED,
{
// ...
}
]
);
static template = hbs`
{{#if this.latestMessage.loading}}
Loading..
{{/if}}
// ...
`;
}
error
This property that can be undefined
or an ApolloError
object, holds the information about any errors that occurred while executing your query. The reported errors are directly reflected from the errorPolicy
option available from Apollo Client.
import { useSubscription } from 'glimmer-apollo';
import {
ON_MESSAGED_ADDED,
OnMessageAddedSubscription,
OnMessageAddedSubscriptionVariables
} from './subscriptions';
export default class LatestMessage extends Component {
latestMessage = useSubscription<
OnMessageAddedSubscription,
OnMessageAddedSubscriptionVariables
>(
this,
() => [
ON_MESSAGED_ADDED,
{
// ...
errorPolicy: 'all'
}
]
);
static template = hbs`
{{#if this.latestMessage.loading}}
Connecting..
{{else if this.latestMessage.error}}
Error!: {{this.latestMessage.error.message}}
{{/if}}
// ...
`;
}
For most cases, it's usually sufficient to check for the loading
state, then the error
state, then finally, assume that the data is available and render it.
promise
This property holds a Promise that resolves when the subscription receives the first data from the network. The Promise will only be updated for the first execution of the Resource, meaning that it won't become an unresolved promise when Apollo Cache is updating or when receiving new events.
As part of the options argument to useSubscription
, you can pass callback functions
allowing you to execute code when a specific event occurs.
onData
This callback gets called when the subscription receives data.
latestMessages = useSubscription(this, () => [
ON_MESSAGED_ADDED,
{
variables: { channel: this.args.channel },
onData: (data): void => {
console.log('Received data:', data);
}
}
]);
onError
This callback gets called when we have an error.
latestMessages = useSubscription(this, () => [
ON_MESSAGED_ADDED,
{
variables: { channel: this.args.channel },
onData: (data): void => {
console.log('Received data:', data);
},
onError: (error): void => {
console.error('Received an error:', error.message);
}
}
]);
onComplete
This callback gets called when the subscription completes its execution. This happens when the server closes the connection for example.
latestMessages = useSubscription(this, () => [
ON_MESSAGED_ADDED,
{
variables: { channel: this.args.channel },
onData: (data): void => {
console.log('Received data:', data);
},
onError: (error): void => {
console.error('Received an error:', error.message);
},
onComplete: (): void => {
console.log('Subscription completed');
}
}
]);
It is often necessary to authenticate a client before allowing it to receive
subscription results. To do this, you can provide a connectionParams
option
to the WebSocketLink
constructor in the Apollo Client setup.
import { WebSocketLink } from '@apollo/client/link/ws';
const wsLink = new WebSocketLink({
uri: 'ws://localhost:3000/subscriptions',
options: {
reconnect: true,
connectionParams: {
authorization: 'Bearer My_TOKEN_HERE'
}
}
});