helfer / apollo-link-debounce

An Apollo Link that debounces requests
MIT License
119 stars 22 forks source link

Optimistic Response with debounce mutations #14

Open jaril5976 opened 3 years ago

jaril5976 commented 3 years ago

code example:

This is optimistic query

return {
    __typename: 'Mutation',
    updateBlockItemField: {
      __typename: 'Resume',
      ...resume,
      id: resumeId,
      updatesCount: resume.updatesCount + 1,
      details: {
        ___typename: 'ResumeDetails',
        ...resume.details,
      },
      settings: {
        ___typename: 'ResumeSettings',
        ...resume.settings,
      },
      blocks: resume.blocks,
    },
  };

This is Mutation:

save = debounce(value => {
    const { mutate, variables, optimisticResponse, update, isCoverLetter } = this.props;
    const options = {
      variables: {
        ...variables,
        value,
      },
      context: {
        debounceKey: generateDebounceKey(variables),
      },
    }

    if (optimisticResponse) options.optimisticResponse = optimisticResponse(value);
    mutate(options);
  }, 30);

And this is my init

return new ApolloClient({
    connectToDevTools: isBrowser,
    ssrMode: !isBrowser, // Disables forceFetch on the server (so queries are only run once)
    link: ApolloLink.from([
      onError(({ graphQLErrors, networkError }) => {
        if (graphQLErrors) {
          graphQLErrors.map(({ message, locations, path }) =>
            console.error(`[GraphQL error]: Message: ${message}, Path: ${path}`),
          );
        }

        if (networkError) {
          console.error(`[${networkError.name}]: Status: ${networkError.statusCode}: Message: ${networkError.message}`);
        }
      }),
      new DebounceLink(1000),
      authLink.concat(
        ApolloLink.split(
          operation => operation.getContext().client === 'coverLetter', // Routes the query to the proper client
          coverLetterLink,
          httpLink,
        ),
      ),
    ]),
    cache,
    resolvers: {},
  });
PeterDekkers commented 2 years ago

I managed to solve this same problem by using custom debouncing, without using apollo-link-debounce.

In a nutshell, for the mutations that I want to debounce, I call a wrapper function which:

  1. Immediately writes the optimistic data to the Apollo cache (using writeFragment). This is an alternative to using optimisticResponse and updates the UI instantly.
  2. Calls a debounced mutation (without optimisticResponse, as the data is already written to the cache).
edgars-sirokovs commented 1 year ago

@PeterDekkers could you share your implementation?

PeterDekkers commented 1 year ago

@edgars-sirokovs, something like this:

import React from 'react';
import debounce from 'lodash/debounce';
import { useApolloClient, useMutation } from '@apollo/client';

const UPDATE_DEBOUNCE_TIME = 1500;

const mutationQuery = gql`
mutation Blah($input: Input!) {
    updateQuery(input: $input) {
        yourQueryHere
    }
}
`;

function useDebouncedMutation() {
    const [doUpdate] = useMutation(mutationQuery);
    const client = useApolloClient();

    const debouncedUpdate = React.useMemo(
        () =>
            debounce(doUpdate, UPDATE_DEBOUNCE_TIME, {
                // Send one immediately
                leading: true,
            }),
        [doUpdate],
    );

    return async ({
        mutationInput
    }) => {
        // Replace this with your way to retrieve the optimistic result
        const optimisticResult = getOptimisticResult(mutationInput);

        const queryVariables = {
            input: mutationInput
        };

        const queryData = {
            doUpdate: {
                __typename: 'UpdateMutation',
                foo: bar,
            },
        };

        // Write to cache for instant UI update.
        // An alternative to `optimisticResponse`, as the mutations are debounced.
        client.writeQuery({
            query: mutationQuery,
            variables: queryVariables,
            data: queryData,
            overwrite: true,
        });

        return debouncedCartUpdate({
            variables: queryVariables,
            ignoreResults: true,
            // Don't write to cache again, we've done that already above.
            // Furthermore, writing to cache here causes a performance
            // issue, where `writeQuery` doesn't immediately update cache
            // when requests are in flight, making the UI seem sluggish.
            // This does mean that the optimistic cart response is taken
            // for truth.
            fetchPolicy: 'no-cache',
        });
    };
}