我最近在开发一些业务逻辑时遇到了这种情况。这是一个简化的人为例子
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 的一堆复选框。它首先从prop派生一个type
数组,称为allTypes
,然后使用该checkedItems
作为初始值来设置自己的状态allTypes
。因此首先检查所有项目。我将ChildComponent
保留为它们自己的状态的原因是该组件可以通过复选框控制它们。但是,因为
propFromApp
最初是一个空数组,所以ChildComponent
的状态checkedItems
也被初始化为一个空数组,并且在更新时不会更新实际数据从API调用返回,这是在
setTimeout
中的回调触发后的500毫秒后。因此,当 Prop 更新时,我使用
useEffect
来更新checkedItems
。所以现在
ChildComponent
看起来像这样
function ChildComponent({ propFromApp }) {
const allTypes = propFromApp.map((item) => item.type)
const [checkedItems, setCheckedItems] = useState(allTypes)
useEffect(() => {
setCheckedItems(allTypes)
}, [allTypes])
return (....)
但这给了一个错误我认为这是因为
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])