ApolloClient v3 fetchMore with nested query results
Asked Answered
S

1

6

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.

Sexed answered 3/12, 2020 at 14:5 Comment(5)
eh....show not working parts ... rendering/component codeAmey
I added the relevant component code. I think the component is working fine, I just think the issue is with the data being returned from the ApolloClient cache. It isn't "changing" and the component doesn't have new data and therefore doesn't rerender.Sexed
eh ... still no rendering code here ... Iguess list wrapped into not changing elements? render only list elements in this component ... if(data) console.log(data.repository.releases.pageInfo.endCursor); ... you can inspect apollo cache store/entries using normal react dev toolsAmey
Added the whole component code. Inspecting apollo cache initially shows no entries, the after one fetchMore it shows 30 entries (initial 15 + new 15) but for further fetchMore requests it still only shows the 30 entries. The component always only shows the first 15 entries. In the network tab, the graphql request continues to fetch all the new entries when fetchMore is fired. I logged the endCursor but it is only shown once, the first time the component renders because it doesn't rerender.Sexed
no if(data) needed if loading/error checked earlier ... destructure/alias and resuse ... const { repository:{ releases }} = data; ... try to force rerender ... return ( <div key={releases.pageinFo.endCursor}> .... releases.nodes.map(...Amey
S
9

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.

Sexed answered 4/1, 2021 at 16:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.