Using document.querySelector in React? Should I use refs instead? How?

前端 未结 2 606
佛祖请我去吃肉
佛祖请我去吃肉 2021-02-14 02:59

I am building a carousel right now, in React. To scroll to the individual slides I am using document.querySelector like so :

useEffect(() => {
           


        
相关标签:
2条回答
  • 2021-02-14 03:28

    Use useLayouEffect instead of useEffect. Follow the link for more information

    0 讨论(0)
  • 2021-02-14 03:34

    I can't answer the "should you" part of whether to use refs for this instead other than if you do, you don't need those id values unless you use them for something else.

    But here's how you would:

    1. Use useRef(null) to create the ref.

      const activeSlideRef = useRef(null);
      
    2. Put it on the Slide that's currently active

      <Slide ref={i === activeSlide ? activeSlideRef : null} ...>
      
    3. In your useEffect, use the ref's current property

      useEffect(() => {
          if (activeSlideRef.current) {
              activeSlideRef.current.scrollIntoView({
                behavior: 'smooth',
                block: 'nearest',
                inline: 'nearest'
              });
          }
      }, [activeSlide]);
      

      (I think activeSlide is a reasonable dependency for that effect. You can't use the ref, the ref itself doesn't vary...)

    Live example, I've turned some of your components into divs for convenience:

    const {useEffect, useRef, useState} = React;
    
    function Deck({children}) {
        const [activeSlide, setActiveSlide] = useState(0);
        const activeSlideRef = useRef(null);
    
        useEffect(() => {
            if (activeSlideRef.current) {
                activeSlideRef.current.scrollIntoView({
                  behavior: 'smooth',
                  block: 'nearest',
                  inline: 'nearest'
                });
            }
        }, [activeSlide]);
    
        const moveLeft = Math.max(0, activeSlide - 1);
        const moveRight = Math.min(children.length - 1, activeSlide + 1);
    
        return (
            <React.Fragment>
              <button onClick={() => setActiveSlide(moveLeft)}>PREV</button>
              <div id="test">
                {children.map((child, i) => {
                  const active = i === activeSlide;
                  return (
                    <div className={`slide ${active ? "active" : ""}`} ref={active ? activeSlideRef : null} id={`slide-${i}`} key={`slide-${i}`}>
                      {child}
                    </div>
                  );
                })}
              </div>
    
              <button onClick={() => setActiveSlide(moveRight)}>NEXT</button>
            </React.Fragment>
        );
    }
    
    ReactDOM.render(
        <Deck>
          <div>slide 0 </div>
          <div>slide 1 </div>
          <div>slide 2 </div>
          <div>slide 3 </div>
          <div>slide 4 </div>
          <div>slide 5 </div>
          <div>slide 6 </div>
          <div>slide 7 </div>
          <div>slide 8 </div>
          <div>slide 9 </div>
        </Deck>,
        document.getElementById("root")
    );
    .slide {
      height: 4em;
      vertical-align: middle;
      text-align: center;
    }
    #test {
      overflow: scroll;
      max-height: 20em;
    }
    .active {
      font-weight: bold;
      color: blue;
    }
    <div id="root"></div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js"></script>


    In a comment you've asked:

    Do you know whether it's possible to disable useEffect here for the first render?

    To keep non-state per-component info around, interestingly you use useRef. The docs for useRef point out that it's not just for DOM element references, it's also for per-component non-state data. So you could have

    const firstRenderRef = useRef(true);
    

    then in your useEffect callback, check firstRenderRef.current &mndash; if it's true, set it false, otherwise do the scrolling:

    const {useEffect, useRef, useState} = React;
    
    function Deck({children}) {
        const [activeSlide, setActiveSlide] = useState(0);
        const activeSlideRef = useRef(null);
        // *** Use a ref with the initial value `true`
        const firstRenderRef = useRef(true);
    
        console.log("render");
    
        useEffect(() => {
            // *** After render, don't do anything, just remember we've seen the render
            if (firstRenderRef.current) {
                console.log("set false");
                firstRenderRef.current = false;
            } else if (activeSlideRef.current) {
                console.log("scroll");
                activeSlideRef.current.scrollIntoView({
                  behavior: 'smooth',
                  block: 'nearest',
                  inline: 'nearest'
                });
            }
        }, [activeSlide]);
    
        const moveLeft = Math.max(0, activeSlide - 1);
        const moveRight = Math.min(children.length - 1, activeSlide + 1);
    
        return (
            <React.Fragment>
              <button onClick={() => setActiveSlide(moveLeft)}>PREV</button>
              <div id="test">
                {children.map((child, i) => {
                  const active = i === activeSlide;
                  return (
                    <div className={`slide ${active ? "active" : ""}`} ref={active ? activeSlideRef : null} id={`slide-${i}`} key={`slide-${i}`}>
                      {child}
                    </div>
                  );
                })}
              </div>
    
              <button onClick={() => setActiveSlide(moveRight)}>NEXT</button>
            </React.Fragment>
        );
    }
    
    ReactDOM.render(
        <Deck>
          <div>slide 0 </div>
          <div>slide 1 </div>
          <div>slide 2 </div>
          <div>slide 3 </div>
          <div>slide 4 </div>
          <div>slide 5 </div>
          <div>slide 6 </div>
          <div>slide 7 </div>
          <div>slide 8 </div>
          <div>slide 9 </div>
        </Deck>,
        document.getElementById("root")
    );
    .slide {
      height: 4em;
      vertical-align: middle;
      text-align: center;
    }
    #test {
      overflow: scroll;
      max-height: 10em;
    }
    .active {
      font-weight: bold;
      color: blue;
    }
    <div id="root"></div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js"></script>


    As a thought experiment, I wrote a hook to make the ergonomics a bit easier:

    function useInstance(instance = {}) {
        // assertion: instance && typeof instance === "object"
        const ref = useRef(instance);
        return ref.current;
    }
    

    Usage:

    const inst = useInstance({first: true});
    

    In useEffect, if inst.first is true, do inst.first = false;; otherwise, do the scrolling.

    Live:

    const {useEffect, useRef, useState} = React;
    
    function useInstance(instance = {}) {
        // assertion: instance && typeof instance === "object"
        const ref = useRef(instance);
        return ref.current;
    }
    
    function Deck({children}) {
        const [activeSlide, setActiveSlide] = useState(0);
        const activeSlideRef = useRef(null);
        const inst = useInstance({first: true});
    
        console.log("render");
    
        useEffect(() => {
            // *** After render, don't do anything, just remember we've seen the render
            if (inst.first) {
                console.log("set false");
                inst.first = false;
            } else if (activeSlideRef.current) {
                console.log("scroll");
                activeSlideRef.current.scrollIntoView({
                  behavior: 'smooth',
                  block: 'nearest',
                  inline: 'nearest'
                });
            }
        }, [activeSlide]);
    
        const moveLeft = Math.max(0, activeSlide - 1);
        const moveRight = Math.min(children.length - 1, activeSlide + 1);
    
        return (
            <React.Fragment>
              <button onClick={() => setActiveSlide(moveLeft)}>PREV</button>
              <div id="test">
                {children.map((child, i) => {
                  const active = i === activeSlide;
                  return (
                    <div className={`slide ${active ? "active" : ""}`} ref={active ? activeSlideRef : null} id={`slide-${i}`} key={`slide-${i}`}>
                      {child}
                    </div>
                  );
                })}
              </div>
    
              <button onClick={() => setActiveSlide(moveRight)}>NEXT</button>
            </React.Fragment>
        );
    }
    
    ReactDOM.render(
        <Deck>
          <div>slide 0 </div>
          <div>slide 1 </div>
          <div>slide 2 </div>
          <div>slide 3 </div>
          <div>slide 4 </div>
          <div>slide 5 </div>
          <div>slide 6 </div>
          <div>slide 7 </div>
          <div>slide 8 </div>
          <div>slide 9 </div>
        </Deck>,
        document.getElementById("root")
    );
    .slide {
      height: 4em;
      vertical-align: middle;
      text-align: center;
    }
    #test {
      overflow: scroll;
      max-height: 10em;
    }
    .active {
      font-weight: bold;
      color: blue;
    }
    <div id="root"></div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js"></script>

    0 讨论(0)
提交回复
热议问题