setState() 不停止渲染

setState() does not stop rendering

我想通过调用映射数组(从数据库调用)的函数来更新 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但效果不佳。

切勿在组件函数的顶层调用状态 setter。对于函数组件,要记住的关键是当你改变状态时,你的函数将以更新后的状态再次调用。如果你的代码在函数的顶层有一个状态变化(就像你在问题中所做的那样),每次函数 运行s,你改变状态,导致函数 运行,导致另一个状态变化,等等,等等。在您的代码中:

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
  • 代码应该在数据库操作未完成时处理组件卸载
  • 代码实际上应该在 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 是一个同步操作。

您在这里看到的是标准的 React 生命周期行为。您的组件将被挂载然后呈现(运行 所有代码都在您的组件中)。第一次渲染后,它会“侦听”您在组件中处理的值的变化,并在检测到任何变化时重新渲染。

您的情况:

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

我在这里看到 2 个不好的地方:

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

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

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

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

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