最近在开发一些业务逻辑的时候遇到了这种情况。这是它的一个简化和人为的例子
function App() {
const [myState, setMyState] = useState([])
useEffect(() => {
setTimeout(() => {
setMyState((prevState) =>
prevState.concat([
{
type: 'foo',
enabled: true,
},
{
type: 'bar',
enabled: true,
},
{
type: 'baz',
enabled: true,
},
])
)
}, 500)
}, [])
return (
<div className="App">
<ChildComponent propFromApp={myState} />
</div>
)
}
我用了setTimeout
模拟 API 调用并将状态传递给 ChildComponent
.
function ChildComponent({ propFromApp }) {
const allTypes = propFromApp.map((item) => item.type)
const [checkedItems, setCheckedItems] = useState(allTypes)
return (
<div>
{allTypes.map((type, index) => (
<div key={index}>
<input
type="checkbox"
name={type}
checked={checkedItems.includes(type)}
onChange={() => {
if (checkedItems.includes(type)) {
setCheckedItems((prevState) =>
prevState.filter((prevType) => type !== prevType)
)
} else {
setCheckedItems((prevState) => prevState.concat(type))
}
}}
/>
<label htmlFor={type}>{type}</label>
</div>
))}
</div>
)
}
和ChildComponent
负责渲染一堆基于 prop 的复选框。它首先派生了一个数组 type
来自 Prop ,称为 allTypes
然后设置自己的状态checkedItems
使用这个allTypes
作为初始值。所以最初所有的项目都会被检查。我做的原因ChildComponent
将它们保持为自己的状态是组件可以通过复选框控制它们。
但是因为 propFromApp
最初是一个空数组,所以 ChildComponent
的状态checkedItems
也被初始化为空数组,并且在以下情况下不会更新
真正的数据从 API 调用返回,这是在 setTimeout
中的回调时 500 毫秒后被解雇了。
然后我使用 useEffect
更新 checkedItems
当 Prop 更新时。
所以现在ChildComponent
看起来像这样
function ChildComponent({ propFromApp }) {
const allTypes = propFromApp.map((item) => item.type)
const [checkedItems, setCheckedItems] = useState(allTypes)
useEffect(() => {
setCheckedItems(allTypes)
}, [allTypes])
return (....)
然而这给出了一个错误
Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
我想是因为allTypes
不是基本类型之一,所以相等性检查总是确定 allTypes
每次组件重新渲染时都是不同的。所以最后我不得不将其更改为使用 useMemo
以避免问题。现在ChildComponent
看起来像这样
function ChildComponent({ propFromApp }) {
const allTypes = useMemo(() => propFromApp.map((item) => item.type), [
propFromApp,
])
const [checkedItems, setCheckedItems] = useState(allTypes)
useEffect(() => {
setCheckedItems(allTypes)
}, [allTypes])
return (....)
现在问题解决了。但我不确定这是正确的做法还是一种好的做法。谁能提出更好的替代方案或指出我的代码哪里不够好?
这里是现场直播 https://codesandbox.io/s/async-sky-3vl1r?file=/src/App.js
最佳答案
useMemo
在这种情况下通常可以解决问题,但也可能不会。 useMemo
可能会也可能不会根据比我聪明的人编写的一些内存管理逻辑重新计算它的值。因此,您需要在编写代码时牢记内存值的引用身份可能会发生变化。引用标识的变化意味着 deps 数组将发生变化。
可以使用 useMemo
修复死循环。在大多数情况下,重新计算只会导致额外的重新渲染。
让 useMemo
影响应用程序逻辑是不好的。在您的示例中,重新计算将重置 checkedItems
而不重新计算则不会。要解决此问题,请推迟派生值,直到您需要导数。
const [checkedItems, setCheckedItems] = useState(() => propFromApp.map((item) => item.type))
useEffect(() => {
const allTypes = propFromApp.map((item) => item.type)
setCheckedItems(allTypes)
}, [propFromApp])
或者,如果您有很多部门,那么密切管理部门可能会变得异常繁重。然后,您可以显式地断开效果回调,除非您期望的更改已经发生。
const prevPropFromApp = useRef(propFromApp)
const propFromAppChanged = prevPropFromApp.current !== (prevPropFromApp.current = propFromApp)
useEffect(() => {
if (!propFromAppChanged) return;
setCheckedItems(allTypes)
}, [allTypes, propFromAppChanged])
关于javascript - 组件的状态源自它的 prop,而该 prop 来自异步 API 调用,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/63543674/