3

My program goes into an infinite loop constantly calling useEffect() everytime I start the app. I have one state that I don't think is changing other than in the retrieveItemStatus() function so I'm confused on why its going into a loop like it is.

const App = () => {
  var items;
  const [itemStatuses, updateStatuses] = useState({});

  const retrieveItemStatus = async () => {
    var tempStatuses;
    try {
      const value = await AsyncStorage.getItem("@item_Statuses");
      if (value !== null) {
        tempStatuses = await JSON.parse(value);
        //console.log("123456");
      } else {
        tempStatuses = await JSON.parse(
          JSON.stringify(require("../default-statuses.json"))
        );
      }
      updateStatuses(tempStatuses);
    } catch (error) {}
  };
  retrieveItemStatus();

  useEffect(() => {
    const copyData = async () => {
      const itemsCopy = [];

      const coll = await collection(db, "Items");
      const querySnapshots = await getDocs(coll);
      const docsArr = querySnapshots.docs;
      docsArr.map((doc) => {
        var data = doc.data();
        if (itemStatuses[data.name] === "locked") return;
        itemsCopy.push(data);
      });
      items = itemsCopy;
      //getItems([...itemsCopy]);
    };

    copyData();

  }, [itemStatuses]);

  return (
    <View style={styles.container}>
      <Text>temp.......</Text>
    </View>
  );
};
skushu
  • 311
  • 1
  • 3
  • 11

3 Answers3

4

It has nothing to do with useEffect. You're calling retrieveItemStatus unconditionally every time your component function is called to render the componennt. retrieveItemStatus calls updateStatuses which changes state. You see your useEffect callback get run repeatedly as a side-effect of that, because your useEffect callback has itemStatuses as a dependency.

I assume you only need the itemStatuses to get fetched once. If so, put the call in a useEffect callback with an empty dependency array:

useEffect(retrieveItemStatus, []);

Also, you have (note the ***):

const App = () => {
  var items // ***
  // ...
  useEffect(() => {
    const copyData = async () => {
      // ...

      items = itemsCopy; // ***

      // ...
    };

    copyData();

  }, [itemStatuses]);
};

That won't work, by the time you assign to items from the callback, anything you might have been trying to do with items will already have just used undefined (the value it gets when you don't give it one). If you need items to be retained, either put it in state (if you use it for rendering) or in a ref (if you don't).


In a comment you said:

Ok so I put retrieveItemStatus() call inside useEffect and removed the dependency which fixed the looping. But now there is an issue where itemStatuses state doesn't get updated before copyData() is called and itemStatuses is needed.. so it doesn't do anything until I manually refresh/render the whole thing again.

If copyData relies on the result from retrieveItemStatus, then put the calls to each of them in the same useEffect, not calling copyData until you get the results from retrieveItemStatus. Something along the lines of the below, though you'll need to tweak it of course as I don't have all the details (I've also made some other comments and changes in there I've flagged up):

// *** There's no need to recreate this function on every render, just
// have it return the information
const retrieveItemStatus = async () => {
    try {
        let tempStatuses; // *** Declare variables in the innermost scope you can
        const value = await AsyncStorage.getItem("@item_Statuses");
        if (value !== null) {
            tempStatuses = await JSON.parse(value);
            //console.log("123456");
        } else {
            // *** stringify + parse isn't a good way to copy an object,
            // see your options at:
            // https://stackoverflow.com/questions/122102/
            tempStatuses = await JSON.parse(JSON.stringify(require("../default-statuses.json")));
        }
        return tempStatuses;
    } catch (error) {
        // *** Not even a `console.error` to tell you something went wrong?
    }
};

// *** Similarly, just pass `itemStatuses` into this function
const copyData = async (itemStatuses) => {
    const coll = await collection(db, "Items");
    const querySnapshots = await getDocs(coll);
    const docsArr = querySnapshots.docs;
    // *** Your previous code was using `map` just as a loop,
    // throwing away the array it creates. That's an anti-
    // pattern, see my post here:
    // https://thenewtoys.dev/blog/2021/04/17/misusing-map/
    // Instead, let's just use a loop:
    // (Alternatively, you could use `filter` to filter out
    // the locked items, and then `map` to build `itemsCopy`,
    // but that loops through twice rather than just once.)
    const itemsCopy = [];   // *** I moved this closer to where
                            // it's actually filled in
    for (const doc of docsArr) {
        const data = doc.data();
        if (itemStatuses[data.name] !== "locked") {
            itemsCopy.push(data);
        }
    }
    //getItems([...itemsCopy]); // *** ?
    return itemsCopy;
};

const App = () => {
    // *** A new `items` is created on each render, you can't just
    // assign to it. You have to make it a member of state (or use
    // a ref if it's not used for rendering.)
    const [items, setItems] = useState(null);
    const [itemStatuses, setItemStatuses] = useState({});
    // ***               ^−−−−− the standard convention is `setXyz`.
    // You don't have to follow convention, but it makes it easier
    // for other people to read and maintain your code if you do.

    useEffect(() => {
        (async () => {
            const newStatuses = await retrieveItemStatus();
            const newItems = await copyData(newStatuses);
            // *** Do you need `itemStatuses` to be in state at all? If it's
            // only used for calling `copyData`, there's no need.
            setItemStatuses(newStatuses);
            setItems(newItems);
        })().catch((error) => {
            console.error(error);
        });
    }, []);

    // *** You didn't show what you're using here, so it's hard to be
    // sure what needs to be in state and what doesn't.
    // Only put `items` or `itemStatuses` in state if you use them for
    // rendering.
    return (
        <View style={styles.container}>
            <Text>temp.......</Text>
        </View>
    );
};

Here are those links as links:

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Ok so I put retrieveItemStatus() call inside useEffect and removed the dependency which fixed the looping. But now there is an issue where itemStatuses state doesn't get updated before copyData() is called and itemStatuses is needed.. so it doesn't do anything until I manually refresh/render the whole thing again. – skushu Aug 17 '22 at 22:02
  • 1
    @skushu - If there's a dependency between the two, do them in the same `useEffect` callback, waiting until `retrieveItemStatus` completes before calling `copyData`. I've updated the answer to show doing that and to flag up a few other issues I saw. – T.J. Crowder Aug 18 '22 at 08:58
2

@T.J. Crowder's answer is correct but I want to clarify a small point, so you understand why your useEffect didn't stop running.

As you you know, when the dependency in dependency array changes, useEffect runs. But the data in your itemStatuses doesn't change right(according to you)? So why does useEffect re-runs? Let's look at the below example:

const obj1 = {};
const obj2 = {};
const obj3 = {a:2};
const obj4 = {a:2};


console.log(obj1 === obj2)
console.log(obj3 === obj4)

console.log(obj3.a === obj4.a)

As you can see javascript doesn't think that an empty object is strictly equal to another empty object. It is because these objects refer to the different locations in memory regardless of their content.

So that's why every time retrieveItemStatus ran, it updated itemStatuses. Then because itemStatuses got updated(although the value is same), useEffect triggered a re-render, so again everything started all over.

Deniz Karadağ
  • 751
  • 3
  • 8
0

Main reason is in calling retrieveItemStatus(); without any event. If you want to call this function when page loading, you should like this.

...
useEffect(() => {
  retrieveItemStatus()
}, [])
...

This will fix loop issue.

Sunderam Dubey
  • 1
  • 11
  • 20
  • 40
Li 1001
  • 65
  • 7