ApolloClient v3 fetchMore with nested query results

2.9k views Asked by At

I'm using ApolloClient 3 the GitHub GraphQL API to retrieve all releases from a repo.

This is what the query looks like:

query ($owner: String!, $name: String!, $first: Int, $after: String, $before: String) {
  repository(owner: $owner, name: $name) {
    id
    releases(orderBy: {field: CREATED_AT, direction: DESC}, first: $first, after: $after, before: $before) {
      nodes {
        name
        publishedAt
        resourcePath
        tagName
        url
        id
        isPrerelease
        description
        descriptionHTML
      }
      totalCount
      pageInfo {
        endCursor
        hasNextPage
        hasPreviousPage
        startCursor
      }
    }
  }
}

This is what the result payload looks like:

Query result payload

This returns me the first x entries (nodes). So far, all good.

I need to implement pagination and I make use of the fetchMore function provided by ApolloClient useQuery. Calling fetchMore fetches the next x entries successfully but these are not displayed in my component list.

According to the ApolloClient Pagination documentation, it seems necessary to handle the merging of the fetchMore results with the ApolloClient caching mechanism. The documentation is understandable for simple situations but I am struggling to implement a solution for the situation where the actual array of results that needs to be merged togeher is deeply nested in the query result (repository -> releases -> nodes).

This is my implementation of the InMemoryCache options merge:

const inMemoryCacheOptions = {
  addTypename: true,
  typePolicies: {
    ReleaseConnection: {
      fields: {
        nodes: {
          merge(existing, incoming, options) {
            const previous = existing || []
            const results = [...previous, ...incoming]
            return results
          }
        }
      }
    },
  }
}

The results array here contains the full list, including the existing entries and the new x entries. This is essentially the correct result. However, my component list which is using the useQuery and fetchMore functionality does not get the new entries after the fetchMore is called.

I have tried various combinations in the inMemoryCacheOptions code above but so far I have been unsuccessful.

To add more context, this is the related component code:

export default function Releases() {
  const { loading, error, data, fetchMore } = useQuery(releasesQuery, {
    variables: {
      owner: "theowner",
      name: "myrepo",
      first: 15
    }
  });

  if (loading) return null;

  if (error) {
    console.error(error);
    return null;
  }

  if (data) {
    console.log(data?.repository?.releases?.pageInfo?.endCursor);
  }

  const handleFetchMore = () => {
    fetchMore({
      variables: {
        first: 15,
        after: data?.repository?.releases?.pageInfo?.endCursor
      }
    });
  };

  return (
    <div>
      <ul>
        {data?.repository?.releases?.nodes?.map(release => (
          <li key={release.id}>{release.name}</li>
        ))}
      </ul>
      <button onClick={handleFetchMore}>Fetch More</button>
    </div>
  );
}

After fetchMore the component doesn't rerender with the new data.

If anyone has any other ideas that I could try, I'd be grateful.

1

There are 1 answers

0
donovantc On BEST ANSWER

I finally managed to solve this. There was no change to the react component code but the InMemoryCacheOptions now looks like this:

const inMemoryCacheOptions = {
  addTypename: true,
  typePolicies: {
    Repository: {
      fields: {
        releases: {
          keyArgs: false,
          merge(existing, incoming) {
            if (!incoming) return existing;
            if (!existing) return incoming;

            const { nodes, ...rest } = incoming;
            // We only need to merge the nodes array.
            // The rest of the fields (pagination) should always be overwritten by incoming
            let result = rest;
            result.nodes = [...existing.nodes, ...nodes];
            return result;
          }
        }
      }
    }
  }
};

The main change from my original code is that I now define the typePolicy for the releases field of the Repository type. Previously I was trying to get directly to the nodes field of the Release type. Since my Repository type the root of the gql query and used in the component, it now reads the merged results from the cache.

If I specified the typePolicy for Query as mentioned in the docs, I would not be able to specify the merge behaviour for the releases field because it would be one level too deep (i.e. Query -> repository -> releases). This is what lead to my confusion in the beginning.