search

Home  >  Q&A  >  body text

It seems useEffect is triggered even though the dependency state has not changed?

<p>I'm trying to build a carousel component in my React project - I'm actually trying to leverage an existing carousel component written in pure JS, using Refs to make it work in React, rather than modifying it directly DOM. I'm running into a problem when I try to initialize the carousel by marking the first slide in the sequence as the current slide (with className current-slide). I put this action in a useEffect hook where {topMovies} is a dependency. {topMovies} is an array of objects passed to the carousel component via prop, so when topMovies changes (i.e. loads), useEffect should run. This works - after loading, the first slide in the carousel is given the classname 'current-slide'. </p> <p>However, the second piece of code (nextSlide and moveToSlide) is the beginning of the function that selects the next slide. Pass the track Ref (i.e. the container of all slides) to the function so that the child element with className 'current-slide' can be selected and its className removed. This also works - but after a short delay (~1 second) the className 'current-slide' reappears.</p> <pre class="brush:php;toolbar:false;">const Carouselv2 = ({topMovies}) => { //Refs const trackRef = useRef(); const nextBtnRef = useRef(); const prevBtnRef = useRef(); const navDotsRef = useRef(); //Initialise carousel upon prop load const [isLoading, setIsLoading] = useState(true); useEffect(() => { if (topMovies && setIsLoading) { const slides = Array.from(trackRef.current.children); slides[0].classList.add('current-slide'); //Mark first slide as visible const slideWidth = slides[0].getBoundingClientRect().width; const setSlidePosition = (slide, index) => { slide.style.left = slideWidth * index 'px'; } slides.forEach(setSlidePosition); //Arrange slides alongside each other setIsLoading(false); } }, [topMovies]); //Button functionality const moveToSlide = (currentSlide, targetSlide) => { //trackRef.current.style.transform = 'translateX(-' targetSlide.style.left ')'; currentSlide.classList.remove('current-slide'); //targetSlide.classList.add('current-slide'); } const updateDots = () => { } const responsiveBtns = () => { } const prevSlide = (e) => { } const nextSlide = (e) => { const currentSlide = trackRef.current.querySelector('.current-slide'); const nextSlide = trackRef.current.children.nextElementSibling; moveToSlide(currentSlide, nextSlide); } return ( <div className="carousel"> <button className="carousel_button carousel_button--left" ref={prevBtnRef}><FontAwesomeIcon icon={faCircleLeft} size="xl" style={{ color: "white" }} /></button> <div className="carousel_track-container"> <ul className="carousel_track" ref={trackRef}> {topMovies && topMovies.map((movie) => ( <li className="carousel_slide"> <img src={movie.image} alt="" /> <h2>{movie.title}</h2> </li> ))} </ul> </div> <button onClick={(e) => nextSlide(e)}className="carousel_button carousel_button--right" ref={nextBtnRef}><FontAwesomeIcon icon={faCircleRight} size="xl" style={{ color: "white" }} /></button> <div className="carousel_nav" ref={navDotsRef}> <button className="carousel_indicator current-slide"></button> {topMovies && Array.apply(0, Array(topMovies.length)).map(() => { return <button className="carousel_indicator"></button> })} </div> </div> ); }</pre> <p>My initial thought was that for some reason, changing className caused useEffect to fire, so I added an if statement with an isLoading condition. But that didn't work, so I don't think the bug comes from the useEffect hook. </p> <p>Very confused by this, but this is my first time using Refs, so it's entirely possible I'm missing something very obvious. Thank you for your help! </p>
P粉946336138P粉946336138471 days ago545

reply all(1)I'll reply

  • P粉642436282

    P粉6424362822023-08-16 10:19:47

    You are mixing DOM created using React and DOM manipulated using pure Javascript. You can't do this, it will just break the DOM/VDOM relationship.

    If you want to avoid moving the entire component to work the way React does, then just use Ref on an empty element, the benefit is that you get the benefits of component mounting and unmounting.

    Then you can pretty much copy-paste the Javascript code and just put it all inside useEffect.

    For example:

    Below I have created two buttons, one controlled entirely by React and the other by JS, when you click on it it will just toggle the pink-button class. I've also placed a checkbox to flip the order of the buttons to cause a re-render to show that the JS version's state is not corrupted, make sure to use the key attribute when doing this to let React know it's the same components. The main point here is to show that React will now no longer interfere with this button, it is now entirely your responsibility. Hope this helps you understand how to mix React with pure JS.

    const domContainer = document.querySelector('#mount');
    const root = ReactDOM.createRoot(domContainer);
    
    const {createRef, useEffect, useState} = React;
    
    function JSButton() {
      const btnRef = createRef(); 
      useEffect(() => {
         //do everything now from inside this useEffect
         const btn = btnRef.current;     
         btn.innerText = 'This is a Javascript Button';     
         const doClick = () => btn.classList.toggle('pink-button');
         btn.addEventListener('click', doClick);     
         return () => {
           //we can also do any clean-up here
           btn.removeEventListner('click', doClick);
         }
      }, []);
      //only render the button component,
      //don't add any props, classes etc.
      //our JS will be handling it all.
      return <button ref={btnRef}/>;
    }
    
    function ReactButton() {
      const [pink, setPink] = useState(false);
      return <button 
        onClick={() => setPink(p => !p)}
        className={pink ? 'pink-button' : ''}>
        React Button
      </button>;
    }
    
    function Test() {
      const [jsFirst, setJsFirst] = useState(false);
      return <React.Fragment>
         <input type="checkbox"
           checked={jsFirst}
           onChange={(e) => {
             setJsFirst(e.target.checked);
           }}
         />Js First
         {jsFirst 
           ? <React.Fragment>
               <JSButton key={'js'}/>
               <ReactButton key={'r'}/>
             </React.Fragment>
           : <React.Fragment>
               <ReactButton key={'r'}/>
               <JSButton key={'js'}/>
             </React.Fragment>       
         }
      </React.Fragment>
    }
    
    root.render(<Test/>);
    .pink-button {
      background-color: pink;
    }
    
    button {
      display: block;
      width: 200px;
      margin: 1rem;
    }
    <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
    
    <div id="mount"></div>

    reply
    0
  • Cancelreply