React Hook - 我总是从 useState 获取过时的值,因为子组件从不更新

React Hook - I always get stale values from useState just because child component never updates

TL;DR 这是我的父组件:

const Parent = () => {

    const [open, setOpen] = useState([]);

    const handleExpand = panelIndex => {

        if (open.includes(panelIndex)) {
            // remove panelIndex from [...open]
            // asign new array to variable: newOpen
            // set the state

            setOpen(newOpen);

        } else {
            setOpen([...open, panelIndex]);
        }
    }

    return (
      <div>
         <Child expand={handleExpand} /> // No need to update
         <Other isExpanded={open} /> // needs to update if open changed
      </div>
    )
}

这是我的 Child 组件:

const Child = (props) => (
   <button
      type="button"
      onClick={() => props.expand(1)}
   >
      EXPAND PANEL 1
   </button>
);

export default React.memo(Child, () => true); // true means don't re-render

这些代码只是一个例子。要点是我不需要更新或重新呈现 Child 组件,因为它只是一个按钮。但是我第二次单击按钮时,它没有触发 Parent 重新渲染。

如果我像这样把 console.log(open) 放在 handleExpand 里面:

const handleExpand = panelIndex => {
    console.log(open);
    if (open.includes(panelIndex)) {
        // remove panelIndex from [...open]
        // asign new array to variable: newOpen
        // set the state

        setOpen(newOpen);

    } else {
        setOpen([...open, panelIndex]);
    }
}

每次我单击按钮时它都会打印出相同的数组,就好像 open 的值从未更新过一样。

但是如果我让 <Child /> 组件在 open 更改时重新渲染,它就可以工作。这是为什么?这符合预期吗?

这确实是预期的行为。

您在这里遇到的是函数闭包。当您将 handleExpand 传递给 Child 时,所有引用的变量都是 'saved' 及其当前值。 open = []。由于您的组件不会重新呈现,因此它不会收到 handleExpand 回调的 'new version'。每次调用都会有相同的结果。

有几种方法可以绕过这个。首先显然是让您的子组件重新渲染。

但是,如果您绝对不想重新渲染,则可以使用 useRef 创建一个对象并访问它的当前 属性:

const openRef = useRef([])
const [open, setOpen] = useState(openRef.current);

// We keep our ref value synced with our state value
useEffect(() => {
  openRef.current = open;
}, [open])

const handleExpand = panelIndex => {    
    if (openRef.current.includes(panelIndex)) {
        setOpen(newOpen);    
    } else {
        // Notice we use the callback version to get the current state
        // and not a referenced state from the closure
        setOpen(open => [...open, panelIndex]);
    }
}