apollo-client: fetchMore's updateQuery option receives the wrong variables

The updateQuery method passed to ObservableQuery.fetchMore is receiving the original query variables instead of the new variables that it used to fetch more data.

This is problematic not so much because it breaks the updateQuery method contract (which is bad, but clearly doesn’t seem to impact many people), but rather because when the results from fetchMore are written to the store in writeQuery, the old variables are used. This cases issues, say, if you were using an @include directive and the variable driving its value changes.

Repro is available here: https://github.com/jamesreggio/react-apollo-error-template/tree/repro-2499

Expected behavior

nextResults.variables shows petsIncluded: true and the pets list gets displayed.

Actual behavior

nextResults.variables shows petsIncluded: null and the pets list is not displayed because the write never happens due to the @include directive being evaluated with the old variables.

Version

  • apollo-client@2.0.1

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 30
  • Comments: 53 (13 by maintainers)

Commits related to this issue

Most upvoted comments

Here’s a GIF to keep this open.

gif

@jbaxleyiii any update on this at all please?

Kind regards,

I think I’ve run into this same issue while trying to do pagination. I’m trying to do

fetchMore({
  variables: {
    page: variables.page + 1
  }
}, ...);

where variables come from data.variables but those never seem to get updated, they are always the initial variables. This causes the code to always attempt to fetch the second page.

It’s 2021 and I am having this issue too. My work around was to use a ref to keep track of the cursor

// wrap useQuery
export function usePaginatedQuery(query, options, updateQuery) {
  const cursorRef = useRef(0) // use a ref since updating it does not cause a re-render
  const results = useQuery(query, options)

  // handle pagination
  const getMore =
    (() =>
      results
        .fetchMore({
          variables: { cursor: ++cursorRef.current },
          updateQuery,
        })

  return {
    ...results,
    getMore,
  }
}

I had this problem and found a workaround for the issue. To give you some context: I was using React and had a button to fetch more posts inside a Query component using cursor-based pagination.

Instead of calling the fetchMore function received from the Query component, I would:

  1. Let my <Query /> component do it’s job with my initial variables or whatever.
  2. Directly call the query method on the client object with different variables and save the response onto a variable named “res”:

const res = await client.query({ query: getPosts, variables: { ...differentVariables })

  1. Use client.readQuery to read from the cache and save it to a variable:

const prev = client.readQuery({ query: getPosts, variables: { ...myVariables })

  1. Lastly, combine the items and write it directly to the cache like so:

const updatedData = { ...prev, posts: [...prev.posts, ...res.data.posts] } client.writeQuery({ query: getPosts, variables: { ...myVariables }, data: updatedData })

This worked flawlessly for me.

same here, I’m using apollo-client@2.2.8

Any chances to fix that 5-months old issue? It seems to be quite significant disfunction. A bit funny considering an activity of Apollo team on various React confs and theirs effort to promote Apollo integrations…

This issue still happening on "apollo-client": "^2.3.7"

In case this might interest someone, I’m currently working around this by passing the necessary paging parameters combined with an updater function from the child component calling the fetchMore, something like this:

const withQuery = graphql(MY_LIST_QUERY, {
  options: ({ pageSize, searchText }) => ({
    variables: {
      pageIndex: 1,
      pageSize,
      searchText: searchText
    }
  }),
  props: ({ data }) => ({
    data: {
      ...data,
      fetchMore: ({ pageIndex, updatePageIndexFn }) => {
        const nextPageIndex = pageIndex + 1;
        updatePageIndexFn(nextPageIndex);

        return data.fetchMore({
          variables: {
            pageIndex: nextPageIndex
          },
          updateQuery: (previousResult, { fetchMoreResult }) => ({
            items: [
              ...previousResult.items,
              ...fetchMoreResult.items
            ]
          })
        });
      }
    }
});

class ListComponent extends React.Component {
  constructor(props) {
    super(props);
    this.pageIndex = props.data.variables.pageIndex;
  }

  handleLoadMore = event => {
    event.preventDefault();
    this.props.data.fetchMore({
      pageIndex: this.pageIndex,
      updatePageIndexFn: index => this.pageIndex = index
    });
  }

  render() {
    return (
      <React.Fragment>
        <ul>{this.props.items.map(item => <li>{JSON.stringify(item, null, 2)}</li>)}</ul>
        <a href="" onClick={this.handleLoadMore}>Load More</a>
      </React.Fragment>
    );
  }
}

const ListComponentWithQuery = withQuery(ListComponent);

edit: What I initially assumed would work is this:

const withQuery = graphql(MY_LIST_QUERY, {
  options: ({ pageSize, searchText }) => ({
    variables: {
      pageIndex: 1,
      pageSize,
      searchText: searchText
    }
  }),
  props: ({ data }) => ({
    data: {
      ...data,
      fetchMore: () => data.fetchMore({
        variables: {
          pageIndex: data.variables.pageIndex + 1
        },
        updateQuery: (previousResult, { fetchMoreResult }) => ({
          items: [
            ...previousResult.items,
            ...fetchMoreResult.items
          ]
        })
      })
    }
});

I have the same issue also after upgrading to apollo-client@^2.3.2

Any update ? I use fetchMore function to create an infinite scroll with the FlatList Component (React Native). As @jamesreggio said, the variables seems not be updated and my infinite scroll turns into an infinite loop of the same query result.

I also notice that queryVariables is null inside the updateQuery callback.

List of the packages I use :

  • “apollo-cache-inmemory”: “1.1.5”
  • “apollo-cache-persist”: “0.1.1”
  • “apollo-client”: “2.2.0”
  • “apollo-link”: “1.0.7”
  • “apollo-link-error”: “1.0.3”
  • “apollo-link-http”: “1.3.2”
  • “apollo-link-state”: “0.3.1”
  • “graphql”: “0.12.3”
  • “graphql-tag”: “2.6.1”
  • “react”: “16.0.0”
  • “react-apollo”: “2.0.4”
  • “react-native”: “0.51.0”
const { data } = this.props;

<FlatList
      data={data.feed}
      refreshing={data.networkStatus === 4}
      onRefresh={() => data.refetch()}
      onEndReachedThreshold={0.5}
      onEndReached={() => {
        // The fetchMore method is used to load new data and add it
        // to the original query we used to populate the list
        data.fetchMore({
          variables: { offset: data.feed.length + 1, limit: 20 },
          updateQuery: (previousResult, { fetchMoreResult,  queryVariables}) => {
            // Don't do anything if there weren't any new items
            if (!fetchMoreResult || fetchMoreResult.feed.length === 0) {
              return previousResult;
            }

            return {
              // Concatenate the new feed results after the old ones
              feed: previousResult.feed.concat(fetchMoreResult.feed),
            };
          },
        });
      }}
    />

There is a workaround for this… or maybe the solution

  1. set fetchPolicy as default
  2. add @connection directive to the field in your query so that apollo can identify which to concat…

like below…

const GET_ITEMS = gql`
  query GetItems($skip: Int, $take: Int, $current: Int) {
    getItems(skip: $skip, take: $take, current: $current) @connection(key: "items") {
      id
      data
      type
      list {
        id
        name
      }
    }
  }
`;

let fetch_options = { skip: 0, take: 2, current: 0 };
export const Pagination: React.FC<Props> = () => {
  const { called, loading, data, fetchMore } = useQuery(GET_ITEMS, {
    variables: fetch_options,
    // fetchPolicy: "cache-and-network", //   Do not set 
  });
  if (called && loading) return <p>Loading ...</p>;
  if (!called) {
    return <div>Press button to fetch next chunk</div>;
  }
  return (
    <div>
      <Button
        onClick={(e: any) => {
          fetch_options = {
            ...fetch_options,
            current: fetch_options.current + 1,
          };
          fetchMore({
            variables: fetch_options,
            updateQuery: (
              previousQueryResult,
              { fetchMoreResult, variables }
            ) => {
              if (!fetchMoreResult) return previousQueryResult;
              return {
                getItems: previousQueryResult.getItems.concat(
                  fetchMoreResult.getItems
                ),
              };
            },
          });
        }}
      >
        Fetch Next
      </Button>
  );
};

Still having a similar issue:

  const nextPage = (skip: number, currentSearch = "") =>
    fetchMore({
      variables: { houseId, skip, search: currentSearch },
      updateQuery: (prev, { fetchMoreResult }) => {
        if (!fetchMoreResult || !prev.allCosts || !fetchMoreResult.allCosts)
          return prev
        return Object.assign({}, prev, {
          allCosts: {
            ...prev.allCosts,
            costs: [...prev.allCosts.costs, ...fetchMoreResult.allCosts.costs],
          },
        })
      },
    })

I call this function when reaching the bottom of the page.

It works perfectly when the search variable remains as “”, when reaching the bottom the skip variable changes and new data is refetched and works great.

However when i change the search variable and refetch the original query, and then try and paginate, i get an “ObservableQuery with this id doesn’t exist: 5” error, I had a look in the source code/added some logs and it seems that apollo is using the old queryId (from the query where the search variable was “”) for the fetchMore. I can see the data being successfully fetched in the network with all the correct variables but saving it back to the cache seems to be where it’s erroring, am i missing something with this implementation?

Note: I am using react-apollo-hooks, which i know isn’t production ready or even provided by apollo-client right now, but from what i’ve seen it looks like its something to do with the fetchMore API

Note: @FunkSoulNinja solution works for me, however would be nice to be able to use the provided API for this kind of feature

@MarttiR You could also use the client object from the Query component render prop function without having to use the withApollo HOC.

This is still an issue on 2.4.13. Could @hwillson consider reopening this?

The workaround by @FunkSoulNinja above works, but becomes convoluted – here’s a more complete example of how it goes together:

import React from "react";
import { Query, withApollo } from "react-apollo";
import gql from "graphql-tag";

const QUERY = gql`
  query someQuery(
    $limit: Int
    $offset: Int
  ) {
    someField(
      limit: $limit
      offset: $offset
    ) {
      totalItems
      searchResults {
        ... on Something {
          field
        }
        ... on SomethingElse {
          otherField
        }
      }
    }
  }
`;

const SearchPage = props => {
  const { client } = props;

  const defaultVariables = {
    limit: 10,
    offset: 0,
  };

  return (
    <Query query={QUERY} variables={defaultVariables}>
      {({ loading, error, data, updateQuery }) => {
        const { someField } = data;
        const { searchResults, totalItems } = someField;
        return (
          <>
            <ResultList results={searchResults} total={totalItems} />
            <Button
              onClick={async () => {
                const nextVariables = Object.assign({}, defaultVariables, {
                  offset: searchResults.length,
                });
                const prevVariables = Object.assign({}, defaultVariables, {
                  offset: searchResults.length - defaultVariables.limit,
                });
                const [next, prev] = await Promise.all([
                  client.query({
                    query: QUERY,
                    variables: nextVariables,
                  }),
                  client.query({
                    query: QUERY,
                    variables: prevVariables,
                  }),
                ]);
                const updatedData = {
                  ...prev.data,
                  someField: {
                    ...prev.data.someField,
                    searchResults: [
                      ...prev.data.someField.searchResults,
                      ...next.data.someField.searchResults,
                    ],
                  },
                };
                updateQuery(
                  (_prev, { variables: prevVariables }) => updatedData
                );
                client.writeQuery({
                  query: QUERY,
                  variables: nextVariables,
                  data: updatedData,
                });
              }}
            >
              Load more
            </Button>
          </>
        );
      }}
    </Query>
  );
};

export default withApollo(SearchPage);

Also heres the fix, test pass and nextResult.variables is correct https://github.com/apollographql/apollo-client/pull/3500

still got this issue… hard to do pagination with refetch now

@jamesreggio as always, thank you for the thoughtful research and repo! I’ll get on it ASAP!