我最近在开发一些业务逻辑时遇到了这种情况。这是一个简化的人为例子

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])

09-25 18:42