【问题标题】:setState() does not stop renderingsetState() 不会停止渲染
【发布时间】:2021-06-22 00:16:39
【问题描述】:

我想通过调用映射通过数组(从数据库调用)的函数来更新 useState 数组值,并且将为(数据库数组)中的每个项目更新 useState 数组,所以我尝试了以下方法:

const [snapshots, setSnapshots] = useState();
const [items, setItems] = useState([]);

// ***  get from the database ***** //

useEffect(()=> {
    db.collection("users").doc("4sfrRMB5ROMxXDvmVdwL").collection("basket")
     .get()
     .then((snapshot) => {
      setSnapshots(snapshot.docs)            
     }
    ) ; 
}, []);

  // ***  get from the database ***** //

  // ***  update items value ***** //
  return <div className="cart__items__item">
            {snapshots && snapshots.map((doc)=>(
            setItems([...items, doc.data().id]),
            console.log(items)
               ))
             }
          </div>   
   // ***  update items value ***** //

但出现以下错误:

Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.

我尝试console.log 来查看结果以查看问题,并且Items 数组连续记录在控制台中我尝试将代码包含在useEffect 中,但效果不佳。

【问题讨论】:

  • useEffect(()=>{setItems(“updated value”)},[ ]) 这可能应该有效。传递空依赖数组,使其仅在挂载后运行(运行一次)
  • 不要在渲染函数中改变状态。通常,您将其称为对用户操作(事件)的反应,或在道具突变后的 useEffect 内。通过直接在渲染函数中调用它,您就是在递归调用函数本身。
  • 请用minimal reproducible example 更新您的问题来证明问题,最好是使用堆栈片段([&lt;&gt;] 工具栏按钮)可运行。 Stack Snippets 支持 React,包括 JSX; here's how to do one.
  • 这里不需要调用setState,你已经在useState中设置了它的值。您在这里所做的只是创建一个无限循环。

标签: javascript arrays reactjs use-effect use-state


【解决方案1】:

永远不要在组件函数的顶层调用状态设置器。对于函数组件,要记住的关键是,当您更改状态时,您的函数将被再次调用并使用更新的状态。如果您的代码在函数的顶层有状态更改(就像您在问题中所做的那样),每次函数运行时,您都会更改状态,导致函数运行,导致另一个状态更改,依此类推,依此类推在。在您的代码中:

const initialArray  = [];                        // *** 1
const [Items, setItems] = useState(initialArray) // *** 2
initialArray.push("pushed item")
setItems(initialArray)                           // *** 3
  1. 每次都创建一个新数组
  2. 创建组件时只使用第一个设置Items的初始值
  3. 设置新数组的状态,导致再次调用函数

相反,您应该只设置状态以响应某些更改或事件,例如单击处理程序或其他一些状态更改等。

还请注意,您不得直接修改您在状态中拥有的对象(包括数组)。从技术上讲,您的代码并没有这样做(因为每次都有一个新的initialArray),但它看起来就像您打算做的那样。要添加到状态数组,您复制数组并在末尾添加新条目。

上面的一个例子:

function Example() {
    const [items, setItems] = useState([]);
    const clickHandler = e => {
        setItems([...items, e.currentTarget.value]);
    };
    return <div>
        <div>
            {items.map(item => <div key={item}>{item}</div>)}
        </div>
        <input type="button" value="A" onClick={clickHandler} />
        <input type="button" value="B" onClick={clickHandler} />
        <input type="button" value="C" onClick={clickHandler} />
    </div>;
}

(为了保持代码示例简单,UI 有点奇怪。)

请注意,通常Items 将被称为items


您的更新:

  • 那段代码在函数的顶层调用setItems,所以出现了上面的问题。相反,您可以在 useEffect 中查询数据库。
  • map 操作期间没有理由重复调用setItems
  • 代码应在 DB 操作未完成时处理组件卸载
  • 代码实际上应该在 JSX 中的 map 中呈现一些内容
  • 代码应处理错误(拒绝)

例如,像这样的:

const [snapshots, setSnapshots] = useState();
const [items, setItems] = useState();   // *** If you're going to use `undefined`
                                        // as the initial state of `snapshots`,
                                        // you probably want to do the same with
                                        // `items`

useEffect(()=> {
    let cancelled = false;
    db.collection("users").doc("4sfrRMB5ROMxXDvmVdwL").collection("basket")
    .get()
    .then((snapshot) => {
        // *** Don't try to set state if we've been unmounted in the meantime
        if (!cancelled) {
            setSnapshots(snapshot.docs);
            // *** Create `items` **once** when you get the snapshots
            setItems(snapshot.docs.map(doc => doc.data().id));
        }
    })
    // *** You need to catch and handle rejections
    .catch(error => {
        // ...handle/report error...
    });
    return () => {
        // *** The component has been unmounted. If you can proactively cancel
        // the outstanding DB operation here, that would be best practice.
        // This sets a flag so that it definitely doesn't try to update an
        // unmounted component, either because A) You can't cancel the DB
        // operation, and/or B) You can, but the cancellation occurred *just*
        // at the wrong time to prevent the promise fulfillment callback from
        // being queued. (E.g., you need it even if you can cancel.)
        cancelled = true;
    };
}, []);

// *** Use `items` here
return <div className="cart__items__item">
    {items && items.map(id => <div>{id}</div>)/* *** Or whatever renders ID */}
</div>;

请注意,该代码假定 doc.data().id 是同步操作。

【讨论】:

  • 好的,但是单击处理程序部分对我不起作用,因为我想在映射通过数组(从数据库调用)的函数中运行此代码,并且将为每个元素更新 items 数组在那个数组中,所以它将面临同样的问题@TJ克劳德
  • @MohammadAmjad - 我们只能回答你写的问题,这与任何问题无关。如果您需要从数据库中获取数据,您可能需要useEffect(如文档所述,这是其主要目的之一),但如果没有更多信息,就不可能更具体。
  • 我已更新问题以澄清问题@T.J.克劳德
  • @MohammadAmjad - 我已经更新了答案。
【解决方案2】:

您在这里看到的是标准的反应生命周期行为。您的组件将被安装然后渲染(运行组件内的所有代码)。在第一次渲染后,它会“监听”你在组件中处理的值的变化,如果检测到任何变化,它会重新渲染。

你的情况:

const initialArray  = [];
const [Items, setItems] = useState(initialArray)
initialArray.push("pushed item")
setItems(initialArray)

我在这里看到了两件坏事:

  1. 您修改了用作状态初始值的数组,并继续使用相同的数组来更新您的状态。
  2. 在调用setItems(initialArray) 时,您推送一个新项目并更新每个渲染的状态

让我们专注于第二个,因为那是导致您的问题的一个。如果您想避免无休止的渲染循环,那么您应该将您的 setItems() 调用移动到一个不会在每次渲染上运行的方法。在功能组件之前,这将在 componentDidMount() 函数中完成。在功能组件中,这是使用 useEffect 挂钩完成的:

useEffect(() => {
  // Your code here
}, [])

注意提供给 useEffect 的空数组。这个数组列出了哪些依赖项应该导致这个 useEffect 运行。如果你把它留空,它只会运行一次,就像旧的 componentDidMount() 函数一样。

所以要解决你的无限渲染问题,你需要将setItems() 移动到 useEffect 钩子中。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2022-01-06
    • 1970-01-01
    • 2016-10-20
    • 2019-01-24
    • 1970-01-01
    • 1970-01-01
    • 2013-08-24
    • 2014-07-09
    相关资源
    最近更新 更多