admin管理员组

文章数量:1415075

Popular ponent renders the Modal, but it will be visible depending on the modal.isVisible Boolean state. My goal is to attach a mouseevent listener and a handler, to close the Modal when clicking outside of it. I tried to do it using useRef, and pass the ref to the Modal dialog, in order to capture the event and check if clicked outside using event.target.contains.

The problem is, on first render I do not want to assign 'mousedown' handler to the document, but only when wrapper && wrapper.current is defined and it outside the Modal dialog. on first render, I can see that the effect runs as expected, but when expanding the Modal by setting isVisible -> true, the ref.current should have changed, but the effect doesn't run again, it will run again if I close the Modal, and then it will work as expecetd. The ref.current changes it not reflected in the effect, even though the effect is supposed to run after the DOM update. Why is that?

const Modal = ({ isVisible, repo, onClose }) => {
    const wrapper = useRef();
    console.count('render modal');

    const escapeHandler = useCallback(({ key }) => {
        if (key == 'Escape') onClose();
    }, [onClose]);

    useEffect(() => {
        document.addEventListener('keydown', escapeHandler);
        return () => document.removeEventListener('keydown', escapeHandler);
    }, []);

    useEffect(() => {
        // runs after first render, but when setting isVisible to true and causing a rerender
        // the effect doesn't run again despite ref.current is changed to <div>
        // only after closing the Modal with Escape, it will work as expected, why?
        console.count('effect modal');
        console.log(wrapper.current);
    }, [wrapper.current]);

    return !isVisible ? null : (
        <div className="modal">
            <div className="modal-dialog" ref={wrapper}>
                <span className="modal-close" onClick={onClose}>&times;</span>
                {repo && <pre>{JSON.stringify(repo, null, 4)}</pre>}
            </div>
        </div>
    );
};

const Popular = () => {
    const [modal, setModal] = useState({ isVisible: false, repo: null });

    const closeModal = useCallback(() => {
        setModal({ isVisible: false, repo: null });
    }, []);

    return <Modal onClose={closeModal} {...modal} />
};

However, after reading the docs, if I use a useCallback and pass it as a ref, it works as expetced, like so, why?

const wrapper = useCallback(node => {
    // works as expected every time the ref changes
    console.log(node);
}, []);

Let me know if the question phrasing is a bit unclear, I'll try to explain a little better

Popular ponent renders the Modal, but it will be visible depending on the modal.isVisible Boolean state. My goal is to attach a mouseevent listener and a handler, to close the Modal when clicking outside of it. I tried to do it using useRef, and pass the ref to the Modal dialog, in order to capture the event and check if clicked outside using event.target.contains.

The problem is, on first render I do not want to assign 'mousedown' handler to the document, but only when wrapper && wrapper.current is defined and it outside the Modal dialog. on first render, I can see that the effect runs as expected, but when expanding the Modal by setting isVisible -> true, the ref.current should have changed, but the effect doesn't run again, it will run again if I close the Modal, and then it will work as expecetd. The ref.current changes it not reflected in the effect, even though the effect is supposed to run after the DOM update. Why is that?

const Modal = ({ isVisible, repo, onClose }) => {
    const wrapper = useRef();
    console.count('render modal');

    const escapeHandler = useCallback(({ key }) => {
        if (key == 'Escape') onClose();
    }, [onClose]);

    useEffect(() => {
        document.addEventListener('keydown', escapeHandler);
        return () => document.removeEventListener('keydown', escapeHandler);
    }, []);

    useEffect(() => {
        // runs after first render, but when setting isVisible to true and causing a rerender
        // the effect doesn't run again despite ref.current is changed to <div>
        // only after closing the Modal with Escape, it will work as expected, why?
        console.count('effect modal');
        console.log(wrapper.current);
    }, [wrapper.current]);

    return !isVisible ? null : (
        <div className="modal">
            <div className="modal-dialog" ref={wrapper}>
                <span className="modal-close" onClick={onClose}>&times;</span>
                {repo && <pre>{JSON.stringify(repo, null, 4)}</pre>}
            </div>
        </div>
    );
};

const Popular = () => {
    const [modal, setModal] = useState({ isVisible: false, repo: null });

    const closeModal = useCallback(() => {
        setModal({ isVisible: false, repo: null });
    }, []);

    return <Modal onClose={closeModal} {...modal} />
};

However, after reading the docs, if I use a useCallback and pass it as a ref, it works as expetced, like so, why?

const wrapper = useCallback(node => {
    // works as expected every time the ref changes
    console.log(node);
}, []);

Let me know if the question phrasing is a bit unclear, I'll try to explain a little better

Share Improve this question asked May 5, 2020 at 10:41 spider monkeyspider monkey 3341 gold badge4 silver badges11 bronze badges
Add a ment  | 

1 Answer 1

Reset to default 5

I am not really sure why despite adding wrapper.current to dependency and ponent re-rendering due to prop change, the useEffect isn't running the first time but is called on all subsequent isVisible state changes. It could be a bug in react. Maybe you can create a issue on reactjs for that.


That said,

However all the other observations you have are justified. Making use of useCallback call the function everytime because in that way you assign the ref using the ref callback pattern wherein ref is assigned like ref={node => wrapper.current = node}.

This way everytime your visible state is true the ref callback is called resulting in the useCallback function to be called if you use it like

const modelRef= useRef(null);
const wrapper = useCallback((node) => {
    modelRef.current = node;
    console.log(node);
}, [])

...

<div ref={wrapper} />

The above useEffect code will however run correctly and deterministically if you add isVisible as a dependency to useEffect instead of wrapper.current

useEffect(() => {
    console.count('effect modal');
    console.log(wrapper.current);
}, [isVisible]) 

and that is a correct way to go too since you ref is changed only when the isVisible flag is changed and depending on a mutable value as a useEffect is not a great solution as there may arise some cases when the mutation isn't acpanied a re-render at the same time and the useEffect won't run at all in that case

本文标签: