React-apollo: Bug : fetchMore doesn't trigger a rerender with the updated data

Created on 27 Oct 2019  路  6Comments  路  Source: apollographql/react-apollo

This bug shows up for example when using react-apollo to lazy-load children of tree nodes.

Using the latest version, here is a small code sample that highlights it :

Schema

type Node {
  id: ID!
  children: [Node!]!
  key: String!
  label: String!
}

type Query {
  nodeChildren(parentKey: String): [Node!]!
}

Client-side gql file

query NodeChildren($parentKey: String) {
  nodeChildren(parentKey: $parentKey) {
    id
    key
    label
  }
}

Demo with react-apollo

const Tree =() => {
  const { data, fetchMore, loading } = useQuery(nodeChildrenQuery, {
    fetchPolicy: 'network-only',
    notifyOnNetworkStatusChange: true,
    variables: {
      parentKey: null
    }
  })

  const theANode = data && data.nodeChildren.find(({ key }) => key === 'A')

  console.log(loading, theANode && theANode.children)

  return (
    <button onClick={() => fetchMore({
      variables: { parentKey: 'A' },
      updateQuery(prev, { fetchMoreResult }) {
        if(!fetchMoreResult) return prev
        theANode.children = fetchMoreResult.nodeChildren
        return { ...prev }
      }
    })}>
      Click me
    </button>
  )
}

On first launch and then by clicking on the button, it outputs the following (without the comments) :

true undefined
false undefined // Good, layer of nodes are fetched without children

// Clicking on the fetchMore button now

true undefined
false undefined // Not good, should show the A node's children

// Clicking on the button a second time

true (4)聽[{鈥, {鈥, {鈥, {鈥] // A bit late
false (4)聽[{鈥, {鈥, {鈥, {鈥]

The problem doesn't come from the server. In fact, as you can see when I click the fetchMore button a second time, the node children are already there, so the first fetchMore did load the children.

The problem is that useQuery rerenders with data that is outdated by one render. In fact, if after the first button click I trigger an update by any other mean (for example a state or props update), the correct data is shown.

This works fine with the old react-apollo-hooks.

Edit : In fact, updateQuery is called after loading is set to false and then there is no rerender at all. So updateQuery doesn't trigger an update at all. When adding a console.log in updateQuery, I get :

true undefined
false undefined

// Clicking on the fetchMore button now

true undefined
false undefined
Inside updateQuery

// Nothing else

All 6 comments

Problems with re-rendering are often caused by mutating data from the cache. Try modifying your example to not mutate the objects retrieved from the cache and see if it helps:

const Tree =() => {
  const { data, fetchMore, loading } = useQuery(nodeChildrenQuery, {
    fetchPolicy: 'network-only',
    notifyOnNetworkStatusChange: true,
    variables: {
      parentKey: null
    }
  })

  return (
    <button onClick={() => fetchMore({
      variables: { parentKey: 'A' },
      updateQuery(prev, { fetchMoreResult }) {
        if(!fetchMoreResult) return prev;
        const theANodeIndex = prev.nodeChildren.findIndex(({ key }) => key === 'A');
        const theANode = prev.nodeChildren[theANodeIndex];
        const newNodeChildren = [...prev.nodeChildren];
        newNodeChildren[theANodeIndex] = { ...theANode, children: fetchMoreResult.nodeChildren };
        return { ...prev, nodeChildren: newNodeChildren };
      }
    })}>
      Click me
    </button>
  )
}

Thanks for your help. Saldy, this still doesn't trigger a rerender, but this time, even when rerendering by force, the new data is not shown. Could this actually have to do with the fact that the children field is not requested in the gql file ? Gonna dig into it.

Ok, I got it. It was actually a combination of two things :

  • As @dylanwulf stated, the return data has to be newly created, and not just shallowly but deeply (which by the way is extremely annoying if you use fetchMore to fetch nested tree data).
  • If the field you're trying to manually add on your data is not listed in your request definition (in my case, it was the children field), it will be discarded anyway. So I had to add it there, even if it always returns null from the server because of lazy loading.

I think I'll go back to the hacky but much easier force-update method, or maybe I'll not use useQuery at all. Thanks for helping.

FYI setting fetchPolicy to no-cache causes the same problem. I had to change it to network-and-cache.

could potentially use lodash's cloneDeep for deep comparison.

I read that this bug was fixed in apollo-client 3 (I did not try it)

If it is need to use fetchPolicy: no-cache you can create new state variable and collect items in it as workaround for older versions of apollo-client:

const [list, setList] = useState([])
const [nextToken, setNextToken] = useState(undefined)

const { data: { list } = {}, fetchMore } = useQuery(gql(GET_LIST), {
    fetchPolicy: "no-cache",
    skip: !!list,
    onCompleted: (data) => {
      setNextToken(data.list.nextToken)
      setUsersList(data.list.items)
    },
  })

const loadMore = async () => {
    if (!nextToken) {
      return null
    }
    return await fetchMore({
      updateQuery: (previousResult, { fetchMoreResult, variables }) => {
        setNextToken(fetchMoreResult.list.nextToken)
        setUsersList([...usersList, ...fetchMoreResult.list.items])
        return null
      },
    })
  }
Was this page helpful?
0 / 5 - 0 ratings