Amos
Amos

Reputation: 1272

Using fetchMore to fetch ALL data on component mount

I have a situation where I need to fetch e.g. all articles posted by a user when a component is mounted. To get a user's articles I am using the following query:

const GET_USER_ARTICLES = gql`
    query getUserArticles($id: ID, $numArticles: Int!, $cursor: String) {
        user(id: $id) {
            id
            articles(first: $numArticles, after: $cursor, orderBy: "-created", state: "enabled") @connection(key: "userArticles") {
                edges {
                    node {
                        name
                    }
                }
                pageInfo {
                    endCursor
                    hasNextPage
                }
            }
        }
    }
`;

If there is a next page I want to keep fetching more articles until I have ALL of them. Up until now I haven't had the need to do anything like this (normally I have a button the user can click "Load more" to fetch more articles for example, but now need to fetch everything without a user interacting with anything), so I'm not sure what the best way to go about this is.

An example of the query in React:

const PAGE_SIZE = 10;

const { data, loading, fetchMore } = useQuery<UserArticlesData, UserArticlesVariables>(
    GET_USER_ARTICLES,
    { variables: { id: userId, numArticles: PAGE_SIZE, cursor: null } },
);

I am a little lost how I can use the fetchMore to keep fetching until there aren't any more pages left, while also showing a loading state to the user. I'm also not sure this is the best way to go about this in the first place, so any suggestions are more than welcome!

Upvotes: 2

Views: 3714

Answers (1)

Daniel Rearden
Daniel Rearden

Reputation: 84687

If the API does not limit the page size, you could just provide an arbitrarily large number as the page size to get the remaining results. Assuming the page size can only be so big, though, you can do something like this:

const { data, loading, fetchMore } = useQuery(GET_USER_ARTICLES, {
  variables: { id: userId, numArticles: PAGE_SIZE, cursor: null },
  notifyOnNetworkStatusChange: true,
})
const fetchRest = async () => {
  const { user: { articles: { pageInfo } } } = data
  const updateQuery = (prev, { fetchMoreResult }) => {
    // Merge the fetchMoreResult and return the combined result
  }

  let hasNextPage = pageInfo.hasNextPage
  let cursor = pageInfo. endCursor

  while (hasNextPage) {
    const { data } = await fetchMore({
      variables: { id: userId, numArticles: PAGE_SIZE, cursor },
      updateQuery,
    })
    const { user: { articles: { pageInfo } } } = data
    hasNextPage = pageInfo.hasNextPage
    cursor = pageInfo. endCursor
  }
}

By setting notifyOnNetworkStatusChange to true, loading will be updated whenever fetchMore is doing any fetching. Then we just loop until hasNextPage is called. fetchMore returns a Promise that resolves to the query result, so we can use the query response outside the updateQuery function.

Note that this is a rough example -- you might actually want to keep track of loading state yourself, for example. If your API has rate limiting, your logic should account for that as well. However hopefully this gives you a good starting point.

Edit:

If you need to get all the articles initially, I wouldn't use useQuery and fetchMore at all. The easiest workaround would be to manage the data and loading state yourself and utilize client.query instead.

const client = useApolloClient()
const [data, setData] = useState()
const [loading, setLoading] = useState(true)
const fetchAll = async () => {
  let hasNextPage = true
  let cursor = null
  let allResults = null

  while (hasNextPage) {
    const { data } = await client.query(GET_USER_ARTICLES, {
      variables: { id: userId, numArticles: PAGE_SIZE, cursor },
    })

    // merge data with allResults

    hasNextPage = pageInfo.hasNextPage
    cursor = pageInfo. endCursor
  }
  setLoading(false)
  setData(allResults)
}

useEffect(() => {
  fetchAll()
}, [])

Upvotes: 4

Related Questions