apollographql / apollo-client

:rocket:  A fully-featured, production ready caching GraphQL client for every UI framework and GraphQL server.
https://apollographql.com/client
MIT License
19.34k stars 2.65k forks source link

useQuery returned same cached data with different variables #9900

Closed Sashkan closed 1 month ago

Sashkan commented 2 years ago

I just setup a small hook using react-router-dom and apollo-client, for a very basic implementation:

This looks just like that:

export const useList = () => {
  const { groupId, viewId } = useParams<{ groupId: string; viewId: string }>();

  const result = useListQuery({
    variables: { groupId, id: viewId },
    skip: !viewId,
    fetchPolicy: 'cache-and-network',
    nextFetchPolicy: 'cache-first',
  });

  return result;
};

export const useListQuery = (
  options?: QueryHookOptions<ListQuery, ListQueryVariables>
) => {
  return useQuery(LIST_QUERY, options)
}

Now, every time I navigate from one list page to another, I still get the old list content before the new one is displayed, resulting in a glitchy effect (previous data appears, then is immediately replaced by the new one)

I made sure that the variables were updated in the query, but still, it seems like apollo will always fetch from the cache for the last call on the query, regardless of the updated variables.

Is there a way to force invalidation? The only workaround I found so far is to set the fetch policy to network-only, which is obviously suboptimal...

multimeric commented 2 years ago

You should also post your cache settings, ie the new ApolloClient() constructor, to make sure that isn't affecting it.

Sashkan commented 2 years ago

Here's the client config:

Cache content:


export function makeCache() {
  return new InMemoryCache({
    typePolicies: {
      Query: {
        fields: {
          listItems: {
            keyArgs: [
              'context',
              'groupId',
              'pagination',
              ['sortField', 'filters', 'sortType', 'query', 'limit'],
              'selection',
            ],
          },
        },
      },
    },
  });
}

Cache


    const cache = makeCache();

    // Main link for HTTP(S) requests.
    const httpLink = split(
      (operation) => operation.operationName === 'contactList',
      new HttpLink({
        uri: `${appConfig.apiUrl}/graphql`,
      }),
      new BatchHttpLink({
        uri: `${appConfig.apiUrl}/graphql`,
      }),
    );

    // WebSocket link.
    const wsLink =
      process.env.REACT_APP_IS_EXTENSION !== 'true' &&
      new WebSocketLink({
        uri: `${appConfig.apiWsUrl}/graphql`,
        options: {
          reconnect: true,
          connectionParams: {
            authorization: `Bearer ${getAccessToken()}`,
            operationName: 'use',
          },
        },
      });

    const link =
      process.env.REACT_APP_IS_EXTENSION === 'true'
        ? httpLink
        : split(
            ({ query }) => {
              const definition = getMainDefinition(query);
              return (
                definition.kind === 'OperationDefinition' &&
                definition.operation === 'subscription'
              );
            },
            wsLink as WebSocketLink,
            httpLink,
          );

    const maintenanceLink = new ApolloLink((operation, forward) => {
      return forward(operation).map((result) => {
        const { pathname, hash, search } = window.location;

        if (pathname !== '/maintenance' && result.errors) {
          const maintenanceError = result.errors.find(
            (err) => err.message === MAINTENANCE_MODE,
          );
          if (maintenanceError) {
            localStorage.setItem(
              'lastLocationBeforeMaintenance',
              JSON.stringify({ pathname, hash, search }),
            );
            window.location.assign('/maintenance');
          }
        }
        return result;
      });
    });

    // Used to persist cached data event after a page reload.
    // Note that user’s cache is flushed if the key changes.
    await persistCache({
      cache,
      storage: new LocalForageWrapper(localForage),
      maxSize: false,
      key: `apollo-cache-persist-${appConfig.apolloCacheVersion || '0.0.0'}`,
    });

    this.client = new ApolloClient({
      assumeImmutableResults: true,
      link: createPersistedQueryLink({ sha256 }).concat(
        ApolloLink.from([
          maintenanceLink,
          onError(({ graphQLErrors }) => {
            // TODO: handle errors
            if (graphQLErrors) {
              graphQLErrors.forEach(({ message, extensions }) => {
                if (
                  extensions?.code === 'UNAUTHENTICATED' ||
                  message === 'authentication-failed' ||
                  message === 'unauthorized-missing-user'
                ) {
                  logout(this.getClient()!).then(() => {
                    redirectTo(UserStatus.NOT_CONNECTED, this.history);
                  });
                }
              });
            }
          }),
          setContext(() => {
            return {
              credentials: 'include',
              headers: {
                authorization: `Bearer ${getAccessToken()}`,
              },
            };
          }),
          link,
        ]),
      ),
      cache,
      name: 'front'
      version: appConfig.version,
    });
multimeric commented 2 years ago

Including the values of the variables you have used in that config (e.g. cache).

Sashkan commented 2 years ago

My bad, I just updated the comment 🙏

Hatko commented 1 month ago

Any updates on this issue?

phryneas commented 1 month ago

I believe this might have been a misconception about useQuery and react-router-dom.

Generally: The useQuery hook will never get data for the wrong variables from the cache. What is does, though, is that while your component is mounted, when you change variables, it holds on to the last data it had until it receives new data for the new variables. The purpose of this is to reduce flickering in your UI - if you go from the first page of a list to the second page, you likely want to keep the old data on the screen and just gray it out instead of going back to an almost-empty screen every time the user clicks "next" and having UI jump around.

Now, per default, if both your old page and your new page have the same component, react-router-dom does not unmount and remount your component - it simply changes props. From an Apollo Client perspective, that looks like you are simply changing variables, there is no way it could distinguish between a "same page with variable change" and "different page" scenario here.

There are multiple things you can do here:

As from our side, this is working as intended, I will close this issue. If you have further usage questions, please also visit our Discord and feel free to ask them in the #frontend channel :)

github-actions[bot] commented 1 month ago

Do you have any feedback for the maintainers? Please tell us by taking a one-minute survey. Your responses will help us understand Apollo Client usage and allow us to serve you better.

github-actions[bot] commented 2 weeks ago

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. For general questions, we recommend using StackOverflow or our discord server.