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>