Manoj Singh
Manoj Singh

Reputation: 2871

Prevent infinite renders when updating state variable inside useEffect hook with data fetched using useQuery of graphql

Graphql provides useQuery hook to fetch data. It will get called whenever the component re-renders.

//mocking useQuery hook of graphql, which updates the data variable
  const data = useQuery(false);

I am using useEffect hook to control how many times should "useQuery" be called.

What I want to do is whenever I receive the data from useQuery, I want to perform some operation on the data and set it to another state variable "stateOfValue" which is a nested object data. So this has to be done inside the useEffect hook.

Hence I need to add my stateOfValue and "data" (this has my API data) variable as a dependencies to the useEffect hook.

  const [stateOfValue, setStateOfValue] = useState({
    name: "jack",
    options: []
  });

  const someOperation = (currentState) => {
    return {
        ...currentState,
        options: [1, 2, 3]
      };
  }
  useEffect(() => {
    if (data) {
      let newValue = someOperation(stateOfValue);
      setStateOfValue(newValue);
    }
  }, [data, stateOfValue]);

Basically I am adding all the variables which are being used inside my useEffect as a dependency because that is the right way to do according to Dan Abramov.

Now, according to react, state updates must be done without mutations to I am creating a new object every time I need to update the state. But with setting a new state variable object, my component gets re-rendered, causing an infinite renders.

How to go about implementing it in such a manner that I pass in all the variables to my dependency array of useEffect, and having it execute useEffect only once.

Please note: it works if I don't add stateOfValue variable to dependencies, but that would be lying to react.

Here is the reproduced link.

Upvotes: 0

Views: 1123

Answers (3)

HMR
HMR

Reputation: 39250

If you want to set state in an effect you can do the following:

const data = useQuery(query);
const [stateOfValue, setStateOfValue] = useState({});
const someOperation = useCallback(
  () =>
    setStateOfValue((current) => ({ ...current, data })),
  [data]
);
useEffect(() => someOperation(), [someOperation]);

Every time data changes the function SomeOperation is re created and causes the effect to run. At some point data is loaded or there is an error and data is not re created again causing someOperation not to be created again and the effect not to run again.

Upvotes: 2

Tony Nguyen
Tony Nguyen

Reputation: 3488

I think you misunderstood what you want to be in dependencies array is [data, setStateOfValue] not [data, stateOfValue]. because you use setStateOfValue not stateOfValue inside useEffect The proper one is:

const [stateOfValue, setStateOfValue] = useState({
    name: "jack",
    options: []
  });

  const someOperation = useCallback((prevValue) => {
    return {
        ...prevValue,
        options: [1, 2, 3]
      };
  },[])
  useEffect(() => {
    if (data) {
      setStateOfValue(prevValue => {
        let newValue = someOperation(prevValue);
        return newValue
      });
    }
  }, [data, setStateOfValue,someOperation]);

Upvotes: 2

Ross Mackay
Ross Mackay

Reputation: 972

First I'd question if you need to store stateOfValue as state. If not (eg it won't be edited by anything else) you could potentially use the useMemo hook instead

const myComputedValue = useMemo(() => someOperation(data), [data]);

Now myComputedValue will be the result of someOperation, but it will only re-run when data changes

If it's necessary to store it as state you might be able to use the onCompleted option in useQuery

const data = useQuery(query, {
  onCompleted: response => {
    let newValue = someOperation();
    setStateOfValue(newValue);
  }
)

Upvotes: 1

Related Questions