即使状态数据没有改变,使用useEffect仍然会陷入无限循环。

3

当我启动应用程序时,我的程序会进入一个无限循环,不断调用useEffect()。我有一个状态,除了在retrieveItemStatus()函数中,我认为它没有改变,所以我很困惑为什么它会像这样进入循环。

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>
  );
};

1
getItems 函数是做什么用的? - Konrad
哎呀,那个本应该被注释掉的。现在已经修复了。 - skushu
3个回答

4
这与 useEffect 无关。你每次调用组件函数渲染组件时都会 无条件地 调用 retrieveItemStatusretrieveItemStatus 调用 updateStatuses改变状态。因此,由于你的 useEffect 回调具有 itemStatuses 作为依赖项的副作用,你会发现回调被重复运行。
我假设你只需要获取itemStatuses一次。如果是这样,请将调用放入一个带有空依赖数组的 useEffect 回调中:
useEffect(retrieveItemStatus, []);

另外,您还有以下内容(请注意***):

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

      items = itemsCopy; // ***

      // ...
    };

    copyData();

  }, [itemStatuses]);
};

那样做是行不通的,当你从回调函数中对items进行赋值时,任何你试图使用items的操作都已经使用了undefined(当你没有给它一个值时它得到的值)。如果你需要保留items,要么将其放在状态中(如果你用它来渲染),要么放在引用中(如果你不这样做)。
在评论中,您说:
好的,所以我将retrieveItemStatus()调用放在useEffect中,并删除了依赖项,解决了循环的问题。但现在有一个问题,就是在调用copyData()之前,itemStatuses state并没有得到更新,而copyData()需要itemStatuses,因此除非我手动刷新/重新渲染整个内容,否则它将不会执行任何操作。
如果copyData()依赖于retrieveItemStatus()的结果,那么将它们各自的调用放在同一个useEffect()中,只有在从retrieveItemStatus()获取结果后再调用copyData()。大致如下,当然你需要根据具体情况进行调整(我还在其中做了一些其他标记和更改):
// *** 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://dev59.com/83VD5IYBdhLWcg3wAGiD
            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>
    );
};

以下是链接:


好的,所以我将 retrieveItemStatus() 调用放在 useEffect 中,并删除了依赖项,以解决循环问题。但现在存在一个问题,在调用 copyData() 之前,itemStatuses 状态不会被更新,而且调用 copyData() 需要 itemStatuses.. 所以它在我手动刷新/重新渲染整个内容之前不会执行任何操作。 - skushu
1
@skushu - 如果两者之间存在依赖关系,请在同一个useEffect回调函数中执行它们,等待retrieveItemStatus完成后再调用copyData。我已更新答案以展示如何做到这一点,并指出我看到的其他一些问题。 - T.J. Crowder

2

@T.J. Crowder的回答是正确的,但我想澄清一个小细节,这样你就能理解为什么你的useEffect没有停止运行。

正如你所知道的,当依赖项数组中的依赖项发生更改时,useEffect会运行。但是根据你的说法,itemStatuses中的数据没有改变,那么为什么useEffect会重新运行呢?让我们看下面的例子:

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)

如您所见,JavaScript不认为空对象严格等于另一个空对象。这是因为这些对象引用内存中的不同位置,而不管它们的内容。

这就是为什么每次运行retrieveItemStatus时,它都会更新itemStatuses。由于itemStatuses被更新(尽管值相同),useEffect触发了重新渲染,因此一切又重新开始了。


0
主要原因是在没有任何事件的情况下调用 retrieveItemStatus();。 如果你想在页面加载时调用这个函数,应该像这样写。
...
useEffect(() => {
  retrieveItemStatus()
}, [])
...

这将解决循环问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接