Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Reactjs: Why doesn't state boolean lock on event listener?

React Noob – thought I’d ask here for a quick answer before I spend hours digging.

In the example below I have an event listener inside useEffect that listens for scroll position on a container and fires a trigger after a point if it hasn’t done so already.

I want to know why the event still triggers even though the boolean registers true in the DOM. I’ve solved the problem by using a normal variable but I think it would benefit me to understand why this is happening. I’ve read lightly into mutating states and have experimented with changing the useState to an object like useState({status: false}) but this had similar results.

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

Even a point in the direction of a reading topic would be enough. Cheers!

const App = (props) => {
    var [stateBool, setStateBool] = React.useState(false);
    var nonStateBool = false;
    var containerRef = React.useRef(null);

    React.useEffect(() => {
        containerRef.current.addEventListener('scroll', (event) => {
            var cont = containerRef.current;
            var triggerPoint =
                cont.scrollWidth - cont.clientWidth - cont.scrollWidth * 0.2;
            var scrollPos = cont.scrollLeft;
            var triggerEl = document.getElementById('vr');
            if (triggerEl) {
                triggerEl.style.left = triggerPoint + 'px';
            }

            if (scrollPos > triggerPoint && stateBool === false) {
                console.log('triggered', nonStateBool, stateBool);
                setStateBool(true);
            }
        });
    }, [containerRef.current, stateBool]);
    return (
        <div>
            <div ref={containerRef} id='container'>
                <div className='divElement'>
                    inner element <vr id='vr'></vr>
                </div>
            </div>
            <ul>
                <li> nonStateBool {nonStateBool ? 'true' : 'false'}</li>
                <li> State Bool {stateBool ? 'true' : 'false'}</li>
            </ul>
        </div>
    );
};

ReactDOM.render(<App />, document.getElementById('root'));
#container {
  width: 200px;
  background: grey;
  padding: 1em;
  overflow: hidden;
  overflow-x: scroll;
}

.divElement {
  width: 1500px;
  height: 50px;
  background: red;
}

.divElement vr {
  border: 1px solid white;
  position: relative;
  height: 100%;
}
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="root"></div>

>Solution :

You have:

React.useEffect(() => {
    containerRef.current.addEventListener('scroll', (event) => {
        // ...
    });
}, [containerRef.current, stateBool]);

So every time stateBool changes, you call addEventListener – adding a new scroll handler. For the more predictable output you’re expecting, remove the previous scroll handler in the effect cleanup.

React.useEffect(() => {
    const handler = (event) => {
        // ...
    };
    containerRef.current.addEventListener('scroll', handler);
    return () => containerRef.current.removeEventListener('scroll', handler);
}, [containerRef.current, stateBool]);
const App = (props) => {
    var [stateBool, setStateBool] = React.useState(false);
    var nonStateBool = false;
    var containerRef = React.useRef(null);

    React.useEffect(() => {
        const handler = (event) => {
            var cont = containerRef.current;
            var triggerPoint =
                cont.scrollWidth - cont.clientWidth - cont.scrollWidth * 0.2;
            var scrollPos = cont.scrollLeft;
            var triggerEl = document.getElementById('vr');
            if (triggerEl) {
                triggerEl.style.left = triggerPoint + 'px';
            }

            if (scrollPos > triggerPoint && stateBool === false) {
                console.log('triggered', nonStateBool, stateBool);
                setStateBool(true);
            }
        };
        containerRef.current.addEventListener('scroll', handler);
        return () => containerRef.current.removeEventListener('scroll', handler);
    }, [containerRef.current, stateBool]);
    return (
        <div>
            <div ref={containerRef} id='container'>
                <div className='divElement'>
                    inner element <vr id='vr'></vr>
                </div>
            </div>
            <ul>
                <li> nonStateBool {nonStateBool ? 'true' : 'false'}</li>
                <li> State Bool {stateBool ? 'true' : 'false'}</li>
            </ul>
        </div>
    );
};

ReactDOM.render(<App />, document.getElementById('root'));
#container {
  width: 200px;
  background: grey;
  padding: 1em;
  overflow: hidden;
  overflow-x: scroll;
}

.divElement {
  width: 1500px;
  height: 50px;
  background: red;
}

.divElement vr {
  border: 1px solid white;
  position: relative;
  height: 100%;
}
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="root"></div>

But, a better approach would be to use the onScroll prop for the container – best to only use vanilla DOM methods like addEventListener when there no reasonable way to achieve the same results through React.

const App = (props) => {
    var [stateBool, setStateBool] = React.useState(false);
    var nonStateBool = false;
    var containerRef = React.useRef(null);
    const scrollHandler = (event) => {
        var cont = containerRef.current;
        var triggerPoint =
            cont.scrollWidth - cont.clientWidth - cont.scrollWidth * 0.2;
        var scrollPos = cont.scrollLeft;
        var triggerEl = document.getElementById('vr');
        if (triggerEl) {
            triggerEl.style.left = triggerPoint + 'px';
        }

        if (scrollPos > triggerPoint && stateBool === false) {
            console.log('triggered', nonStateBool, stateBool);
            setStateBool(true);
        }
    };
    return (
        <div>
            <div ref={containerRef} onScroll={scrollHandler} id='container'>
                <div className='divElement'>
                    inner element <vr id='vr'></vr>
                </div>
            </div>
            <ul>
                <li> nonStateBool {nonStateBool ? 'true' : 'false'}</li>
                <li> State Bool {stateBool ? 'true' : 'false'}</li>
            </ul>
        </div>
    );
};

ReactDOM.render(<App />, document.getElementById('root'));
#container {
  width: 200px;
  background: grey;
  padding: 1em;
  overflow: hidden;
  overflow-x: scroll;
}

.divElement {
  width: 1500px;
  height: 50px;
  background: red;
}

.divElement vr {
  border: 1px solid white;
  position: relative;
  height: 100%;
}
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="root"></div>
Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading