setState() 不会停止渲染

时间:2021-03-25 08:08:39

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

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

2 个答案:

答案 0 :(得分:4)

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

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

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

另请注意,您不得直接修改处于状态的对象(包括数组)。您的代码在技术上并没有这样做(因为每次都有一个新的 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 中完成这项工作。
  • 没有理由在 setItems 操作期间重复调用 map
  • 代码应在数据库操作未完成时处理组件卸载
  • 代码实际上应该在 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 是同步操作。

答案 1 :(得分:1)

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

您的情况:

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 钩子中。