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
- 每次创建一个新数组
- 创建组件时只使用第一个设置
Items
的初始值
- 在状态中设置新数组,导致再次调用该函数
相反,您应该仅在响应某些更改或事件时设置状态,例如单击处理程序或其他一些状态更改等。
另请注意,您不得直接修改状态中的对象(包括数组)。您的代码在技术上并没有这样做(因为每次都有一个新的 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 个不好的地方:
- 您修改用作状态初始值的数组,并继续使用相同的数组来更新您的状态。
- 您在调用
setItems(initialArray)
时推送一个新项目并更新每个渲染的状态
让我们关注第二个问题,因为这是导致您出现问题的原因。如果你想避免无休止的渲染循环,那么你应该将你的 setItems()
调用移动到一个不会在每个渲染上 运行 的方法。在功能组件之前,这将在 componentDidMount()
函数中完成。在功能组件中,这是使用 useEffect 挂钩完成的:
useEffect(() => {
// Your code here
}, [])
注意提供给 useEffect 的空数组。该数组列出了哪些依赖项会导致此 useEffect 达到 运行。如果你把它留空,它只会 运行 一次,就像旧的 componentDidMount() 函数一样。
所以要解决你无限渲染的问题,你需要将 setItems()
移到 useEffect 钩子中。
我想通过调用映射数组(从数据库调用)的函数来更新 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
- 每次创建一个新数组
- 创建组件时只使用第一个设置
Items
的初始值 - 在状态中设置新数组,导致再次调用该函数
相反,您应该仅在响应某些更改或事件时设置状态,例如单击处理程序或其他一些状态更改等。
另请注意,您不得直接修改状态中的对象(包括数组)。您的代码在技术上并没有这样做(因为每次都有一个新的 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 个不好的地方:
- 您修改用作状态初始值的数组,并继续使用相同的数组来更新您的状态。
- 您在调用
setItems(initialArray)
时推送一个新项目并更新每个渲染的状态
让我们关注第二个问题,因为这是导致您出现问题的原因。如果你想避免无休止的渲染循环,那么你应该将你的 setItems()
调用移动到一个不会在每个渲染上 运行 的方法。在功能组件之前,这将在 componentDidMount()
函数中完成。在功能组件中,这是使用 useEffect 挂钩完成的:
useEffect(() => {
// Your code here
}, [])
注意提供给 useEffect 的空数组。该数组列出了哪些依赖项会导致此 useEffect 达到 运行。如果你把它留空,它只会 运行 一次,就像旧的 componentDidMount() 函数一样。
所以要解决你无限渲染的问题,你需要将 setItems()
移到 useEffect 钩子中。