Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimistic update for subscriptions #5267

Closed
bkniffler opened this issue Sep 2, 2019 · 15 comments
Closed

Optimistic update for subscriptions #5267

bkniffler opened this issue Sep 2, 2019 · 15 comments

Comments

@bkniffler
Copy link

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".

@elitan
Copy link

elitan commented Sep 3, 2019

Wondering the same, also using Hasura. Follow.

@bkniffler
Copy link
Author

For hasura, there seems to exist is a workaround, as suggested by @heyrict here: hasura/graphql-engine#2317 (comment)

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.

@jorroll
Copy link

jorroll commented Dec 30, 2019

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.

In an Angular app, 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.

@switz
Copy link

switz commented Feb 3, 2020

@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.

@geekox86
Copy link

Greetings Apollo team.

Any plans to make subscriptions updatable from the cache after mutations?

@nolandg
Copy link

nolandg commented Jan 22, 2021

As mentioned above you kinda have to use both subscriptions and queries to get both live updates and optimistic responses. This is how I do it with React hooks alone.

I have a functional React component that uses both useQuery and useSubscription and passes both the same document (minus the subscription/query string diff). I then 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.

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
}

@jdgamble555
Copy link

@nolandg
This does not work as postSubscriptionResult never gets called.

@nolandg
Copy link

nolandg commented Jan 26, 2021

@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.

@jdgamble555
Copy link

jdgamble555 commented Jan 27, 2021

@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.

@nolandg
Copy link

nolandg commented Jan 27, 2021

@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.

@SamuelC54
Copy link

SamuelC54 commented Feb 19, 2021

Because I didn't know to expand my Apollo client like what @thefliik did and because I use React, I instead created a custom hook to create the same behavior as his example (having the subscription data in the cache to be able to modify the cache and create optimistic update). Note that my logic is made to be used with Hasura but it's very easy to change it for any subscription technologies.

The hook

export const useHasuraSubscriptionWithCache = (
  queryDocument: Apollo.DocumentNode,
  options?: any
) => {
  const queryString = queryDocument.loc.source.body;
  const subscriptionDocument = gql(
    queryString.replace('query', 'subscription')
  );

  const queryDocumentResult = Apollo.useQuery(queryDocument, {
    variables: options?.variables,
  });

  useEffect(() => {
    if (queryDocumentResult?.subscribeToMore) {
      const unsubscribe = queryDocumentResult.subscribeToMore({
        document: subscriptionDocument,
        updateQuery: (_, curr) => {
          return curr.subscriptionData.data;
        },
        variables: options?.variables,
      });
      return () => unsubscribe();
    }
  }, [options?.variables, queryDocumentResult, subscriptionDocument]);

  return queryDocumentResult;
};

How to use it

const scheduleProjectsData = useHasuraSubscriptionWithCache(
    ScheduleProjectsDataDocument
  );

How to change the cache in the mutation to create an optimistic update

projectDateUpdate({
    variables: {
      input: {
        id: id,
        start: startDate,
        end: endDate,
      },
    },
    update: (cache) => {
      const data = cache.readQuery({
        query: ScheduleProjectsDataDocument,
      });

      if (!data) {
        return;
      }

      cache.writeQuery({
        query: ScheduleProjectsDataDocument,
        data: {
          ...data,
          projects: calculateOptimisticProjectMoveData(
            data.projects,
            id,
            startDate,
            endDate
          ),
        },
      });
    },
  });

@EmrysMyrddin
Copy link

Is there any plan to fix this issue ?
I see a lot of workaround here, which is great for a temporary fix. But this is still in my opinion a bug.

When we use a subscription, each value received will update the apollo client cache, and therefore trigger a rerender on every useQuery with the updated value.
But when the cache is updated (by either a useQuery or an useMutation or an optimistic update, the value of useSubscription` is not updated and doesn't trigger en rerender.

It is, in my opinion, misleading that the cache update only work in one way. It should always work, or never, but not something between.

Is there any plan to fix this? Would a PR on this subject be appreciated?

I don't know if this kind of behavior changes is ok for a minor version ?

@jorroll
Copy link

jorroll commented Apr 8, 2021

@EmrysMyrddin FWIW, I would also love to see this changed but I definitely think it would be breaking and require a major version bump. I think the current implementation is intentional and hence, even if we disagree with it, not a "bug".

When subscriptions were first rolled out, the impression I got is that their intended use was as small, real-time "supplements" to "normal" http requests. The idea was that apps like github, which are largely not realtime, could add small realtime features (like, in real time, adding the dot next to the notifications icon when a new notification comes in). These subscriptions were subscribing to events, rather than data (e.g. the "notification added" event).

Today though, subscriptions are often used as a mechanism for subscribing to real-time data rather than to an event (live queries were suppose to be our method for subscribing to real-time data, but the GraphQL spec moves slow and people are impatient and we all just co-opted subscriptions. Last I checked [admittedly a while ago], the GraphQL spec has now largely said, "well if subscriptions work I guess we don't need to do anything else after all."--if you're unfamiliar, the problem with subscriptions is that they return everything each time they fire, rather than just the diff like a "live query" was suppose to do).

If you're subscribing to events, it makes sense that you only return events as they happen, and nothing from the cache. If you subscribe to data though, then this doesn't make sense (you'll generally want an optimistic update immediately). So I think the current Apollo strategy made sense at the time, but is now just outdated. Hence, it's appropriate to change the implementation but it definitely is a breaking change. Alternatively, providing some way to "opt-in" to optimistic subscription results would be a way to slide this functionality in as a minor release. This is probably the best solution.

@jdgamble555
Copy link

I had a similar argument with URQL. It seems they are not going to fix this since the GraphQL spec does not say it is necessary. The GraphQL spec needs to change first apparently, although I don't understand what the spec specifically says about this: urql-graphql/urql#1423

@alessbell
Copy link
Member

Hey all 👋 Thanks for the discussion here! I've just read through the linked urql discussion and, without restating them all here, I agree with the points raised by its maintainers about why optimistic updates don't make sense within the programming model of subscriptions.

That being said, our team at Apollo has been thinking about the future of real-time data in GraphQL (in fact, that was the title of @benjamn's keynote delivered at GraphQL Summit this past October 😄 video, slides). To stay up to date (pun intended... 🥁), consider joining our Discord as we share more in the coming months.

For now, I'm going to close this issue out, but thank you for being a part of the Apollo community and hope to see you online!

@alessbell alessbell removed the 🏓 awaiting-team-response requires input from the apollo team label Jan 12, 2023
@jerelmiller jerelmiller closed this as not planned Won't fix, can't repro, duplicate, stale Jan 12, 2023
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Feb 14, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests