apollographql / react-apollo

:recycle: React integration for Apollo Client
https://www.apollographql.com/docs/react/
MIT License
6.85k stars 787 forks source link

useQuery infinite loop re-rendering #3644

Open chanm003 opened 4 years ago

chanm003 commented 4 years ago

Intended outcome:

The useQuery hook fires. If the GQL results in error, I use a library to raise a toast notification inside of Apollo client's onError callback. This should not cause functional component to re-render.

Actual outcome:

The functional component re-renders. The useQuery hook issues another network request. Apollo client's onError callback runs, another toast fires. Resulting in infinite loop of network request, error, and toast.

This might be similar to the class-based components infinite loop problem that could occur when a component's render method invokes setState resulting in infinite loop of re-renders

How to reproduce the issue:

Following code example works: https://codesandbox.io/s/blissful-hypatia-9qsoy?fontsize=14

The below GQL is valid and works as I intend:

const GET_ITEMS = gql`
  {
    continents {
      name
    }
  }
`;

but if you were to alter the GQL to something invalid as shown below, the infinite loop issue occurs:

const GET_ITEMS = gql`
  {
    invalidGQL {
      name
    }
  }
`;
dylanwulf commented 4 years ago

Could be related to #3595

pawelkleczek10c commented 4 years ago

yup, there's even test for that https://github.com/apollographql/react-apollo/blob/master/packages/hoc/src/__tests__/queries/lifecycle.test.tsx#L448-L665

strblr commented 4 years ago

Having the same problem. The infinite loop keeps on going even when the component containing the useQuery is unmounted. I have a pollInterval on my query and the query is rerun at that rate.

PaulRBerg commented 4 years ago

@ostrebler pollInterval causes refetches on top of the already existing re-rendering problem. Disable it to see for yourself.

zerocity commented 4 years ago

I had this also. I solved it with replacing the the useQuery hook with the query component and the behavior was gone

mlecoq commented 4 years ago

Hi, I also have an infinite loop on error. In my case removing <React.StrictMode> has stopped this issue

alexnofoget commented 4 years ago

Has someone solved this problem? I also have the same issue

danielmaartens-sovtech commented 4 years ago

Same here, except I have an infinite loop even on a successful response !

My component keeps re-rendering once my graphQL query has successfully returned a response.

This re-render fires off the query again, and once the query has returned the data the component is re-rendered.

And this keeps on going infinitely.

Changing the poll interval to 0 does not fix the issue.

Here is my usage of the useQuery hook in my functional component:

const {data: cataloguePageCount, loading: pageCountLoading} = useQuery(getCataloguePageCount, { client: apiClient, });

And my graphql query: query getCataloguePageCount { pageCount: getCataloguePageCount }

yuzhenmi commented 4 years ago

I'm having this issue as well. For me, this was caused by having a query variable that changes on every render - I have a timestamp as a variable that is determined relative to now. Rounding the timestamp to the nearest minute stopped the loop for me.

kevloves commented 4 years ago

Any updates on this? Same problem here. Getting an infinite re-rendering whenever the query fires on click.

jesuslopezlugo commented 4 years ago

I also noticed this issue using loader of graphql.macro to load .graphql files instead of using gql from graphql-tag.

I found a solution of my infinite loop issue, just passing an empty function to onCompleted property of useQuery and problem solved 😕

import React from "react";
import {FormattedMessage} from 'react-intl';
import {loader} from 'graphql.macro';
import {useQuery} from '@apollo/react-hooks';

const Workspace = () => {
    const GET_WORKSPACE = loader('./../../../graphql/Workspace/get_workspace.graphql');
    const {loading, error, data, refetch} = useQuery(
        GET_WORKSPACE,
        {
            variables: {user_id: "12345"},
            onCompleted: data => { }
        }
    );
    if (loading) return 'Loading...';
    if (error) return `Error! ${error.message}`;
    return (
        <div style={{padding: 24, background: '#fff', minHeight: 360}}>
            <h2><FormattedMessage id="dashboard.tasks.title" defaultMessage="Tasks"/></h2>
            {data.workspace.map(item => (
                <span key={item.id}> {item.name}</span>
            ))}
            <button onClick={() => refetch()}>Refetch!</button>
        </div>
    );
};
export default Workspace;
mixkorshun commented 4 years ago

It's probably problem issued by graphql.macro and any other GraphQL loaders (like babel-plugin-graphql-tag in my case). This plugins compiles GraphQL query to static object, which will be recreated every re-render cycle. To prevent this behaviour you can do following:

jesuslopezlugo commented 4 years ago

It's probably problem issued by graphql.macro and any other GraphQL loaders (like babel-plugin-graphql-tag in my case). This plugins compiles GraphQL query to static object, which will be recreated every re-render cycle. To prevent this behaviour you can do following:

  • extract query declaration from render function
    const GET_WORKSPACE = loader('./../../../graphql/Workspace/get_workspace.graphql');
    const Workspace = () => {
       const {loading, error, data, refetch} = useQuery(GET_WORKSPACE, {
           variables: {user_id: "12345"}
       });
       // continue rendering
    };
  • use memoization
    const Workspace = () => {
       const GET_WORKSPACE = React.useMemo(() => loader('./../../../graphql/Workspace/get_workspace.graphql'), []);
       const {loading, error, data, refetch} = useQuery(GET_WORKSPACE, {
           variables: {user_id: "12345"}
       });
       // continue rendering
    };

Thanks, @mixkorshun that solve my issue with graphql.macro 😎🙌🏻

gregorskii commented 4 years ago

I'm having this issue as well. For me, this was caused by having a query variable that changes on every render - I have a timestamp as a variable that is determined relative to now. Rounding the timestamp to the nearest minute stopped the loop for me.

I had this same issue, using a timestamp in a function that built the query caused it to re-render every time. I also saw this happen on failure though.

KamilOcean commented 4 years ago

Guys, I'm not sure., But I had similar issue with an infinite loop and in my case, it helped for me.

This is a copy of my answer from Stack Overflow: https://stackoverflow.com/questions/59660178/overcome-endless-looping-when-executing-usequery-apolloclient-by-defining-a-ne/61817054#61817054

I just exclude new ApolloClient from render function.

Actually, I don't see render function from your code, but in my case, it was something like this:

Before

export default function MyComponent () {
  const anotherClient = new ApolloClient({
    uri: "https://my-url/online-service/graphql"
  });
  const { data, loading } = useQuery(QueryKTP, {client: anotherClient});
}

After

const anotherClient = new ApolloClient({
  uri: "https://my-url/online-service/graphql"
});
export default function MyComponent () {
  const { data, loading } = useQuery(QueryKTP, {client: anotherClient});
}

In my case, it helped. You should know, that in similar cases just look to new keyword. For example, guys often meet the same bug with an infinite loop, when they use new Date() in the render function

smeijer commented 4 years ago

I'm having a similar problem, and did find a temporary solution. But Oh-My, this was hard to trace down.

After upgrading @apollo/client to the v3 range, my (local) server was being spammed by requests. Not directly though, it started after half a minute.

Turned out, it was a poll event that triggered it. I still don't know why or how, but I do know how to work around it.

The initial render went fine. It's only after the poll is being triggered, that the query gets stuck in a loop. The component itself doesn't get rendered though. React doesn't paint any updates, and logging statements around the query aren't being written either.

const { data, refetch } = useMyQuery({
  skip: !itemId,
  variables: { itemId },
  // pollInterval: 15 * 1000,
});

useEffect(() => {
  const id = setInterval(() => {
    refetch();
  }, 15 * 1000);

  return () => clearInterval(id);
}, [refetch]);

That's my workaround. If I uncomment pollInterval and remove the useEffect, the query will be thrown in that infinite loop after 15 seconds.

ps. I'm using graphql-code-generator.com to generate that useMyQuery.

update

I've simplified my workaround to import useInterval from beautiful-react-hooks.

const { data, refetch } = useMyQuery({
  skip: !itemId,
  variables: { itemId },
});

useInterval(refetch, 15 * 1000);

Something seems to be broken inside the pollInterval.

ilovepumpkin commented 4 years ago

My workaround is also to leverage the "skip" property.

The code looks like below:

const [mydata,setMyData]=useState(null)
const {data, loading,error}=useQuery(MyQuery,{
    variables:{itemId},
    skip: !!mydata
})
if(data){
  setMyData(data)
}
// use mydata to render UI
fromi commented 4 years ago

I face this issue too, with two queries actually. First query is like that: { me { id name avatar authenticated } } Second query is like that: { me { games {...GameInfo} } } When application opens, the first query is executed once, everything works fine. As soon as we click on the menu to see the games, the second query runs... and there goes the trouble: it causes the first query to run again, which causes the second to run again, etc. I guess it relates to some cache invalidation, but I request complementary fields of the same query. Is it the intended behavior? I don't want to query the user's game in the same query because it requires database access and makes things too slow at startup.

Edit: just upgraded from @apollo/client 3.0.0-beta.50 to 3.0.0-rc.10 and bug is solved apparently. Now it only triggers the first query once (which I don't need, however it causes no more harm)

Tautorn commented 4 years ago

I had this problem when I use fetchPolicy, with useQuery and useLazyQuery. I just remove from arguments. Work for me. I don't know why are with this problem now.

damikun commented 4 years ago

When i delete (or im not using) query option "OnError" or "OnComplete" i dont have loop problem... but when one or any other is present then infinity render loop is available.. @benjamn is this going to be planned to fix?

I found another related same topics.. Im adding hire to have in place for anothers...

https://github.com/apollographql/apollo-client/issues/6634 https://github.com/apollographql/react-apollo/issues/4044 https://github.com/apollographql/react-apollo/issues/4000