valentin Ivanov
valentin Ivanov

Reputation: 83

GraphQL defaulting data causes infinite loops in useEffect

so I have this code

const [localState, setLocalState] = useState<StateType[]>([]);
const { data = { attribute: [] }, loading } = useQuery<DataType>(QUERY, {
  variables: {
    id: client && client.id
  },
  skip: user.clients && user.clients.length === 0
});

useEffect(() => {
  if (loading || !data) {
    return undefined;
  }

  if (data && data.attribute) {
    const sortedResult = data.attribute.sort((a, b) =>
      a.updatedAt < b.updatedAt ? 1 : -1
    );
    setLocalState(sortedResult);
  }
}, [data]);

the issue is when useQuery returns empty(undefined) result and data defaults to {attribute: []} useEffect keeps being triggered for forever, however when useQuery returns data (so it is not defaulted) useEffects is being entered only once. The solution for this problem was only to remove default parameter = {attribute: []} in the query so it looks like this:

const [localState, setLocalState] = useState<StateType[]>([]);
const { data, loading } = useQuery<DataType>(QUERY, {
  variables: {
    id: client && client.id
  },
  skip: user.clients && user.clients.length === 0
});

useEffect(() => {
  if (loading || !data) {
    return undefined;
  }

  if (data && data.attribute) {
    const sortedResult = data.attribute.sort((a, b) =>
      a.updatedAt < b.updatedAt ? 1 : -1
    );
    setLocalState(sortedResult);
  }
}, [data]);

Why defaulting parameter in useQuery makes useEffect being triggered for infinity?

(Important note to add - I tried to remove sort function, thinking that it mutates data object and causes reentering but it didn't change anything)

Upvotes: 2

Views: 880

Answers (2)

xadm
xadm

Reputation: 8418

const { data = { attribute: [] }, loading } = useQuery<DataType>(QUERY, {

This declaration doesn't make a larger sense:

  • not required as all rendering [and calculations] should be (and already is) blocked by data checking first;
  • what with deeper nested values? next empty declarations? It leads to a dead-end;

It can create a new data object on every render ... but it shouldn't cause infinite rerenderings ... some other reasons?

You can break the effect loop by:

const [localState, setLocalState] = useState(null);

useEffect(() => {
  if (data && data.attribute && !localState) {

You can also use onCompleted event in hook:

const { loading } = useQuery<DataType>(QUERY, {
  variables: {
    id: client && client.id
  },
  skip: user.clients && user.clients.length === 0
  onCompleted: (data) => {
    if(data && data.attribute) {
      const sortedResult = data.attribute.sort((a, b) =>
        a.updatedAt < b.updatedAt ? 1 : -1
      );
      setLocalState(sortedResult);
    }
  }
});

... check/use loading and {localState && <SomeResultView data={localState} />}

Upvotes: 0

kigiri
kigiri

Reputation: 3368

Your data is a new object at each render, as such re-triggering your useEffect that re-triggers an render and so on.

I don't think you need a useEffect here:

    const { data, loading } = useQuery<
        DataType
    >(QUERY, {
        variables: {
            id: (client && client.id)
        },
        skip: user.clients && user.clients.length === 0
    });

    const localState = (!loading && data && data.attribute)
        ? undefined
        : data.attribute.sort(
              (a, b) => (a.updatedAt < b.updatedAt ? 1 : -1)
          );

should be enough

Upvotes: 2

Related Questions