Skip to content
Advertisement

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.

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>

Advertisement

Answer

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>
Advertisement