I've got a subscription (a hasura server live-query) like:
subscription get_items {
item {
id
name
}
}
When doing a mutation with optimisticResponse set, the subscription will not be updated optimistically. If I use a the same gql, but instead as a query, everything works perfectly as expected. I guess that subscription cache is currently not updated by optimisticResponse. Also, manual cache updating will not work since subscription results can't be retrieved by the cache (no cache.readSubscription).
Is there a particular reason why subscriptions don't have any optimistic capabilities? Is this a bug or more of a feature request? Sorry for not filling the whole template. I can go and create a reproducible example, but I'm not yet sure if absence of optimism in subscriptions is "by design".
Wondering the same, also using Hasura. Follow.
For hasura, there seems to exist is a workaround, as suggested by @heyrict here: https://github.com/hasura/graphql-engine/issues/2317#issuecomment-527330779
Basically you'll have a query instead of a subscription and you'll use the queries subscribeToMore capability to your subscription. The query can then be updated by optimisticResponse and will also be updated by the subscription.
I still think that subscriptions should have some sort of cache API like queries do and they should be automatically updated by optimisticResponse.
So I've been running into this as well (also using Hasura). In my case, I found that subscription
queries never return results from the cache. Digging into this, it appears that this is intended (it is obliquely referenced here when they say "Subscriptions are similar to queries...but instead of immediately returning a single answer, a result is sent every time a particular event happens on the server").
As pointed out by @bkniffler, as well as kinda mentioned in the ApolloClient docs, if you want to have a GQL subscription interact with the cache (and generally behave the way ApolloClient queries behave), you need to use the subscribeToMore
option of watchQuery()
(it's actually an option on the return object of watchQuery()
). The option doesn't have great documentation, but can be seen here. A downside of this approach is that, for each subscription operation, you need to provide two GQL documents (a query
document and a subscription
document). For Hasura users, these documents are almost identical.
I decided to simplify this by extending ApolloClient
and adding a new liveQuery()
method.
class CustomApolloClient extends ApolloClient {
liveQuery<T, V>(options: Omit<WatchQueryOptions<V>, 'pollInterval'>) {
const { query } = options;
const queryString = query.loc!.source.body;
const subscription = gql(queryString.replace('query', 'subscription'));
const watchQuery = this.watchQuery<T, V>(options);
let subscribeToMoreTeardown: (() => void) | undefined;
return watchQuery.valueChanges.pipe(
tapOnSubscribe(() => {
subscribeToMoreTeardown = watchQuery.subscribeToMore({
document: subscription,
updateQuery: (_, curr) => curr.subscriptionData.data,
variables: options.variables,
});
}),
finalize(() => {
subscribeToMoreTeardown!();
}),
share(),
);
}
}
/** Triggers callback every time a new observer subscribes to this chain. */
function tapOnSubscribe<T>(callback: () => void): MonoTypeOperatorFunction<T> {
return (source: Observable<T>): Observable<T> =>
defer(() => {
callback();
return source;
});
}
This method receives a single query
operation document and generates a subscription
operation document to pair with it. It than constructs a watchQuery
observable which handles subscribeToMore-ing results from the database. The result of this is that you can use liveQuery()
to subscribe to data from Hasura and it utilizes the cache in the same manner as a watchQuery()
.
Hope this helps.
@thefliik hey this is really great. I'm curious how this has worked out for you so far in production, are you happy with this solution? I'm planning on doing something similar.
Greetings Apollo team.
Any plans to make subscriptions updatable from the cache after mutations?
I may have found a simpler work-around but haven't dug deep enough to determine if this is fortunate unintended behaviour that may be removed at some point. If someone with a deeper understanding of Hasura and Apollo understands why this works, please let us know.
I have a functional React component that uses both useQuery
and useSubscription
and passes both the same document (minus the subscription
/query
string diff). Each document has the same Operation Name. All I do then is use optimisticResponse
in my mutation and the local agent who made the mutation updates instantly and other clients update after about 1 subscription latency, like 1 second I think. The React UI is using the query result and ignores the subscription result.
I tried to simplify and clean up a code example below but realized post
is a poor example. We're actually fetching a single database row which contains grouping data for a list (job board). I hope you get the idea though.
```jsx
const postQueryString =
query FetchJobBoardPost{
jobBoardPosts: post_by_pk(id: "1234"){
id title
}
}
;
const POST_QUERY = gql
${groupingQueryString};
const POST_SUBSCRIPTION = gql
${gpostQueryString.replace('query', 'subscription')}`;
const JobBoard = (props) => {
const [updatePostMutate] = useMutation(UPDATE_POST_MUTATION);
const postQueryResult = useQuery(POST_QUERY);
const postSubscriptionResult = useSubscription(POST_SUBSCRIPTION);
const handleButtonClick = () => {
const optimisticResponse = {
__typename: 'Mutation',
updatePost: {
id: '123',
__typename: 'post',
title: newTitle,
},
};
updatePostMutate({
variables: { newTitle },
optimisticResponse,
});
}
// React UI here using postQueryResult
}
@nolandg
This does not work as postSubscriptionResult never gets called.
@jdgamble555 Not sure what you mean. postSubscriptionResult
is not a function that can be called. It's the result object returned by the Apollo Client query hook. It has props like loading
, data
, error
etc. By calling the hook, you run the query (unless you specify skip = true
). I don't think the fact that it's not referenced later affects the whether the query is run. In another heavily optimized compiled language it might be removed but JS doesn't appear to do that.
@nolandg - I apologize for my ignorance, as I am an angular, typescript, and svelte developer, not react...
So I am assuming the fact that it is not reference indicates the same thing as if subscribe()
is called in svelte, typescript, or angular... will try it that way...
Since it is not referenced, I imagine you could just run useSubscription(POST_SUBSCRIPTION);
on its own without setting it as a variable to confuse things.
@jdgamble555 Yes, you could just call useSubscription(POST_SUBSCRIPTION);
without assigning the result but it's a pain for debugging, hard to monitor it with console, etc. I liked to compare the results of each in the console.
I suppose not assigning it though would make the fact that there are intended side effects more obvious.
Most helpful comment
So I've been running into this as well (also using Hasura). In my case, I found that
subscription
queries never return results from the cache. Digging into this, it appears that this is intended (it is obliquely referenced here when they say "Subscriptions are similar to queries...but instead of immediately returning a single answer, a result is sent every time a particular event happens on the server").As pointed out by @bkniffler, as well as kinda mentioned in the ApolloClient docs, if you want to have a GQL subscription interact with the cache (and generally behave the way ApolloClient queries behave), you need to use the
subscribeToMore
option ofwatchQuery()
(it's actually an option on the return object ofwatchQuery()
). The option doesn't have great documentation, but can be seen here. A downside of this approach is that, for each subscription operation, you need to provide two GQL documents (aquery
document and asubscription
document). For Hasura users, these documents are almost identical.I decided to simplify this by extending
ApolloClient
and adding a newliveQuery()
method.This method receives a single
query
operation document and generates asubscription
operation document to pair with it. It than constructs awatchQuery
observable which handles subscribeToMore-ing results from the database. The result of this is that you can useliveQuery()
to subscribe to data from Hasura and it utilizes the cache in the same manner as awatchQuery()
.Hope this helps.