How does fetchMore return data to the component?

Viewed 4338

I am trying to follow the example of cursor-based paginating with React Apollo (https://www.apollographql.com/docs/react/data/pagination/#cursor-based) but am struggling with how my component that rendered the original data gets the new (appended) data.

This is how we get the original data and pass it to the component:

const { data: { comments, cursor }, loading, fetchMore } = useQuery(
    MORE_COMMENTS_QUERY
);

<Comments
      entries={comments || []}
      onLoadMore={...}
/>

What I'm unsure of is how the fetchMore function works.

onLoadMore={() =>
        fetchMore({
          query: MORE_COMMENTS_QUERY,
          variables: { cursor: cursor },
          updateQuery: (previousResult, { fetchMoreResult }) => {
            const previousEntry = previousResult.entry;
            const newComments = fetchMoreResult.moreComments.comments;
            const newCursor = fetchMoreResult.moreComments.cursor;

            return {
              // By returning `cursor` here, we update the `fetchMore` function
              // to the new cursor.
              cursor: newCursor,
              entry: {
                // Put the new comments in the front of the list
                comments: [...newComments, ...previousEntry.comments]
              },
              __typename: previousEntry.__typename
            };
          }
        })
      }

From what I understand, yes, once my component will cal this onLoadMore function (using a button's onClick for example), it will fetch the data based on a new cursor.

My question is this. I'm sorry if this is too simple and I'm not understanding something basic.

How does the component get the new data?

I know the data is there, because I console logged the newComments (in my case, it wasn't newComments, but you get the idea.) And I saw the new data! But those new comments, how are they returned to the component that needs the data? And if I click the button again, it is still stuck on the same cursor as before.

What am I missing here?

2 Answers

It depends on how you handle the offset. I'll try to simplify an example for you.

This is a simplified component that I use successfully:

const PlayerStats = () => {
  const { data, loading, fetchMore } = useQuery(CUMULATIVE_STATS, {
    variables: sortVars,
  })

  const players = data.GetCumulativeStats

  const loadMore = () => {
    fetchMore({
      variables: { offset: players.length },
      updateQuery: (prevResult, { fetchMoreResult }) => {
        if (!fetchMoreResult) return prevResult
        return {
          ...prevResult,
          GetCumulativeStats: [
            ...prevResult.GetCumulativeStats,
            ...fetchMoreResult.GetCumulativeStats,
          ],
        }
      },
    })
  }

My CUMULATIVE_STATS query returns 50 rows by default. I pass the length of that result array to my fetchMore query as offset. So when I execute CUMULATIVE_STATS with fetchMore, the variables of the query are both sortVars and offset.

My resolver in the backend handles the offset so that if it is, for example, 50, it ignores the first 50 results of the query and returns the next 50 from there (ie. rows 51-100).

Then in the updateQuery I have two objects available: prevResult and fetchMoreResult. At this point I just combine them using spread operator. If no new results are returned, I return the previous results.

When I have fetched more once, the results of players.length becomes 100 instead of 50. And that is my new offset and new data will be queried the next time I call fetchMore.

In the updateQuery function lets you modify (override) the result for the current query. At the same time your component is subscribed to the query and will get the new result. Let's play this through:

  • Your component is rendered for the first time, component will subscribe to the query and receive the current result of the query from the cache if there is any. If not the query starts fetching from the GraphQL server and your component gets notified about the loading state.
  • If the query was fetched your component will get the data once the result came in. It now shows the first x results. In the cache an entry for your query field is created. This might look something like this:
{
  "Query": {
    "cursor": "cursor1",
    "entry": { "comments": [{ ... }, { ... }] }
  }
} 

// normalised
{
  "Query": {
    "cursor": "cursor1",
    "entry": Ref("Entry:1"),
  }
  "Entry:1": {
    comments: [Ref("Comment:1"), Ref("Comment:2")],
  },
  "Comment:1": { ... },
  "Comment:2": { ... }
}
  • User clicks on load more and your query is fetched again but with the cursor value. The cursor tells the API from which entry it should start returning values. In our example after Comment with id 2.
  • Query result comes in and you use the updateQuery function to manually update the result of the query in the cache. The idea here is that we want to merge the old result (list) with the new result list. We already fetched 2 comments and now we want to add the two new comments. You have to return a result that is the combined result from two queries. For this we need to update the cursor value (so that we can click "load more" again and also concat the lists of comments. The value is written to the cache and our normalised cache now looks like this:
{
  "Query": {
    "cursor": "cursor2",
    "entry": { "comments": [{ ... }, { ... }, { ... }, { ... }] }
  }
}

// normalised
{
  "Query": {
    "cursor": "cursor2",
    "entry": Ref("Entry:1"),
  }
  "Entry:1": {
    comments: [Ref("Comment:1"), Ref("Comment:2"), Ref("Comment:3"), Ref("Comment:4")],
  },
  "Comment:1": { ... },
  "Comment:2": { ... },
  "Comment:3": { ... },
  "Comment:4": { ... }
}
  • Since your component is subscribed to the query it will get rerendered with the new query result from the cache! The data is displayed in the UI because we merged the query so that the component gets new data just as if the result had all four comments in the first place.
Related