[英]When accessing a state variable from a useCallback, value is not updated
At a certain place of my code I'm accessing a state variable of my component from a call back ( UserCallback
) and I find the state variable has not updated from the initial value and call back is referring to the initial value.在我的代码的某个位置,我正在从回调(
UserCallback
)访问我的组件的 state 变量,我发现 state 变量尚未从初始值更新,回调指的是初始值。 As I read in the documentation when variable is passed as one of array items then it should update the function when it is updated.正如我在文档中读到的,当变量作为数组项之一传递时,它应该在更新时更新 function。 Following is a sample code.
以下是示例代码。
const Child = forwardRef((props, ref) => {
const [count, setCount] = useState(0);
const node = useRef(null);
useImperativeHandle(ref, () => ({
increment() {
setCount(count + 1);
}
}));
const clickListener = useCallback(
e => {
if (!node.current.contains(e.target)) {
alert(count);
}
},
[count]
);
useEffect(() => {
// Attach the listeners on component mount.
document.addEventListener("click", clickListener);
// Detach the listeners on component unmount.
return () => {
document.removeEventListener("click", clickListener);
};
}, []);
return (
<div
ref={node}
style={{ width: "500px", height: "100px", backgroundColor: "yellow" }}
>
<h1>Hi {count}</h1>
</div>
);
});
const Parent = () => {
const childRef = useRef();
return (
<div>
<Child ref={childRef} />
<button onClick={() => childRef.current.increment()}>Click</button>
</div>
);
};
export default function App() {
return (
<div className="App">
<Parent />
</div>
);
}
What I'm originally building is a custom confirmation modal.我最初构建的是自定义确认模式。 I have a state variable which set either display:block or display:none to the root element.
我有一个 state 变量,它将display:block或display:none设置为根元素。 Then if there is a click outside the component I need to close the modal by setting state variable to false .
然后,如果在组件外部单击,我需要通过将 state 变量设置为false来关闭模式。 Following is the original function.
以下是原 function。
const clickListener = useCallback(
(e: MouseEvent) => {
console.log('isVisible - ', isVisible, ' count - ', count, ' !node.current.contains(e.target) - ', !node.current.contains(e.target))
if (isVisible && !node.current.contains(e.target)) {
setIsVisible(false)
}
},
[node.current, isVisible],
)
It doesn't get closed because isVisible is always false which is the initial value.它不会关闭,因为isVisible始终为false ,这是初始值。
What am I doing wrong here?我在这里做错了什么?
For further clarifications following is the full component.为了进一步澄清,以下是完整的组件。
const ConfirmActionModal = (props, ref) => {
const [isVisible, setIsVisible] = useState(false)
const [count, setCount] = useState(0)
const showModal = () => {
setIsVisible(true)
setCount(1)
}
useImperativeHandle(ref, () => {
return {
showModal: showModal
}
});
const node = useRef(null)
const stateRef = useRef(isVisible);
const escapeListener = useCallback((e: KeyboardEvent) => {
if (e.key === 'Escape') {
setIsVisible(false)
}
}, [])
useEffect(() => {
stateRef.current = isVisible;
}, [isVisible]);
useEffect(() => {
const clickListener = e => {
if (stateRef.current && !node.current.contains(e.target)) {
setIsVisible(false)
}
};
// Attach the listeners on component mount.
document.addEventListener('click', clickListener)
document.addEventListener('keyup', escapeListener)
// Detach the listeners on component unmount.
return () => {
document.removeEventListener('click', clickListener)
document.removeEventListener('keyup', escapeListener)
}
}, [])
return (
<div ref={node}>
<ConfirmPanel style={{ display : isVisible ? 'block': 'none'}}>
<ConfirmMessage>
Complete - {isVisible.toString()} - {count}
</ConfirmMessage>
<PrimaryButton
type="submit"
style={{
backgroundColor: "#00aa10",
color: "white",
marginRight: "10px",
margin: "auto"
}}
onClick={() => {console.log(isVisible); setCount(2)}}
>Confirm</PrimaryButton>
</ConfirmPanel>
</div>
)
}
export default forwardRef(ConfirmActionModal)
You assign a function clickListener
to document.addEventListener
on component mount , this function has a closure on count
value.您在组件 mount上将 function
clickListener
分配给document.addEventListener
,这个 function 对count
数值有一个关闭。
On the next render, the count
value will be stale.在下一次渲染时,
count
数值将是陈旧的。
One way to solve it is implementing a function with refernce closure instead:解决它的一种方法是使用参考闭包来实现 function:
const Child = forwardRef((props, ref) => {
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
countRef.current = count;
}, [count]);
useEffect(() => {
// countRef.current always holds the most updated state
const clickListener = e => {
if (!node.current.contains(e.target)) {
alert(countRef.current);
}
};
document.addEventListener("click", clickListener);
return () => {
document.removeEventListener("click", clickListener);
};
}, []);
...
}
While your clickListener
does change when count
changes you only bind the initial clickListener
once on mount because your useEffect
dependency list is empty.虽然当
count
更改时您的clickListener
确实会更改,但您只在安装时绑定初始clickListener
一次,因为您的useEffect
依赖项列表为空。 You could ad clickListener
to the dependency list as well:您也可以将
clickListener
添加到依赖项列表中:
useEffect(() => {
// Attach the listeners on component mount.
document.addEventListener("click", clickListener);
// Detach the listeners on component unmount.
return () => {
document.removeEventListener("click", clickListener);
};
}, [clickListener]);
Side note: using node.current
in a dependency list doesn't do anything as react does not notice any changes to a ref.旁注:在依赖列表中使用
node.current
不会做任何事情,因为 react 不会注意到对 ref 的任何更改。 Dependencies can only be state or props.依赖项只能是 state 或 props。
You can pass a callback to setIsvisible so you don't need isVisible
as a dependency of the useCallback
.您可以将回调传递给 setIsvisible,因此您不需要
isVisible
作为useCallback
的依赖项。 Adding node.current
is pointless since node is a ref and gets mutated:添加
node.current
是没有意义的,因为 node 是一个 ref 并且会发生变异:
const clickListener = useCallback((e) => {
setIsVisible((isVisible) => {//pass callback to state setter
if (isVisible && !node.current.contains(e.target)) {
return false;
}
return isVisible;
});
}, []);//no dependencies needed
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.