【问题标题】:React state resets when set from prop event handler从 prop 事件处理程序设置时,React 状态重置
【发布时间】:2020-01-19 22:23:49
【问题描述】:

我一生都无法弄清楚发生了什么,但是由于某种原因,当单击“单击我”时,数字会按您的预期增加。当子组件触发点击时,它会重置状态并始终打印 0。

function Child(props: {
    onClick?: (id: string) => void,
}) {
    const ref = useCallback((ref) => {
        ref.innerHTML = 'This Doesnt';
        ref.addEventListener('click',() => {
            props.onClick!('')
        })
    }, [])
    return (<div ref={ref}></div>)
}

function Parent() {
  const [number, setNumber] = useState(0);
  return <div>
            <div onClick={() => {
                setNumber(number + 1);
                console.log(number);
            }}>
                This Works
            </div>
            <Child
                onClick={(id) => {
                    setNumber(number + 1);
                    console.log(number);;
                }}
            />
        </div>
}

这里是问题的演示:https://jscomplete.com/playground/s333177

【问题讨论】:

  • SVGView中useCallback()的原因是什么?我想知道这是否可能是您的问题的原因?
  • 获取对 div 的引用。
  • 在这种情况下,reactjs.org/docs/hooks-reference.html#useref 可能更合适
  • 使用 ref 也有同样的问题..
  • props.onClick!('') 的轰鸣对于 Typescript 有什么特别的吗?另外,为什么要将onClick 包装在一个闭包中,而不是直接将它作为回调参数传递给addEventListener

标签: javascript reactjs react-hooks


【解决方案1】:

父组件中的onClick 处理程序在每次渲染时都会重新创建,因为它们在number 状态字段上有一个闭包。

问题是,ref 回调中使用了发送给 Child 组件的 onClick 属性,由于依赖列表为空,因此仅在初始渲染时才反应。所以 Child 在后续渲染中收到的 onClick 属性根本不会被使用。

尝试通过删除依赖项参数或在依赖项列表中发送 props.onClick 来解决此错误,由于文档中提到的警告,我们遇到了问题。 https://reactjs.org/docs/refs-and-the-dom.html

所以你添加了 null 处理,你会看到你更新的回调现在被调用了,但是......所有早期的回调也被调用了,因为我们没有删除那些事件监听器。

const ref = useCallback((ref) => {
    if(!ref) return;
    ref.innerHTML = 'This Doesnt';
    ref.addEventListener('click',() => {
        props.onClick!('')
    })
}, [props.onClick])

我相信这只是作为学习钩子的一部分进行的实验,否则没有必要迂回地从 ref 回调中调用onClick。只需将其作为道具传递给div

编辑:
根据您的评论,这不仅仅是一个实验,而是简化了一些需要通过addEventListener 设置点击处理程序的真正要求,这是一个可能的解决方案:

const ref = useRef(null);
useEffect(() => {
    if(!ref.current) return;
    ref.current.innerHTML = 'This Doesnt';
    const onClick = () => props.onClick!('');
    ref.current.addEventListener('click',onClick)

    // return the cleanup function to remove the click handler, which will be called before this effect is run next time.
    return () => {ref.current.removeEventListener("click", onClick)}
}, [ref.current, props.onClick]);

基本上,我们需要使用useEffect,这样我们就有机会在添加新的侦听器之前删除旧的侦听器。

希望这会有所帮助。

【讨论】:

  • 所有这一切的推理都在我的简化中丢失了。我有一个 SVG 视图类,它对 SVG 进行大量操作,因此使用 snapsvg。我正在向各种 svg 元素添加点击事件,并且一直在尝试避免每次道具更改时解析和重新渲染整个 svg 但看起来如果我要使用钩子,我最好的选择是重新建立 onclick每次渲染后的处理程序。在这种情况下可能只是恢复为使用一个类。
  • 我已经用可能的解决方案更新了答案。请检查。
  • 糟糕。刚刚注意到@kwdowik 提供了类似的解决方案。
  • 谢谢。这基本上就是我最终做的事情。实际上,这一切最终都比仅仅使用一个类更混乱,但至少它很有趣!再次感谢。
【解决方案2】:
function Child(props: {
    onClick?: (id: string) => void,
}) {
    function handleClick() {
      props.onClick('')
    }
    const ref = useRef();
    useEffect(() => {
      ref.current.innerHTML = 'This Doesnt';
      ref.current.addEventListener('click', handleClick)
      return () => { ref.current.removeEventListener('click', handleClick); }
    }, [props.onClick])

    return (<div ref={ref}></div>)
}

@ckder 几乎可以工作,但 console.log 显示了从 0 到当前数值的所有数字。 问题在于事件侦听器在子组件卸载后尚未删除,因此为了实现这一点,我使用了 useEffect 并在我卸载侦听器的地方返回函数。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-03-29
    • 1970-01-01
    • 2017-01-08
    • 1970-01-01
    • 2014-01-27
    • 1970-01-01
    • 2018-04-10
    相关资源
    最近更新 更多