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. 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. 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. Then if there is a click outside the component I need to close the modal by setting state variable to false. Following is the original 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.
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)