【问题标题】:Using document.querySelector in React? Should I use refs instead? How?在 React 中使用 document.querySelector?我应该改用 refs 吗?如何?
【发布时间】:2020-03-30 14:37:43
【问题描述】:

我现在正在用 React 构建一个轮播。要滚动到我正在使用document.querySelector 的单个幻灯片,如下所示:

useEffect(() => {
    document.querySelector(`#slide-${activeSlide}`).scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'nearest'
    });
  }, [activeSlide]);

这是不好的做法吗?毕竟我这里是直接访问 DOM 的?这样做的 React 方式是什么?

编辑:完整的return 方法

return (
    <>
      <button onClick={() => setActiveSlide(moveLeft)}>PREV</button>
      <Wrapper id="test">
        {children.map((child, i) => {
          return (
            <Slide id={`slide-${i}`} key={`slide-${i}`}>
              {child}
            </Slide>
          );
        })}
      </Wrapper>

      <button onClick={() => setActiveSlide(moveRight)}>NEXT</button>
    </>
  );

【问题讨论】:

  • 您滑入视图的元素是否由您的组件渲染?
  • 是的。我将编辑我的帖子以包含完整的返回块

标签: javascript reactjs ref use-effect js-scrollintoview


【解决方案1】:

我无法回答是否使用 refs 来代替“你应该”的部分,除非你这样做,除非你将它们用于其他用途,否则你不需要这些 id 值。

但你可以这样做:

  1. 使用useRef(null) 创建引用。

    const activeSlideRef = useRef(null);
    
  2. 把它放在当前活跃的Slide

    <Slide ref={i === activeSlide ? activeSlideRef : null} ...>
    
  3. 在您的useEffect 中,使用引用的current 属性

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

    (我认为activeSlide 是该效果的合理依赖项。您不能使用 ref,ref 本身不会变化...)

活生生的例子,为了方便,我把你的一些组件变成了divs:

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>

在您提出的评论中:

你知道第一次渲染时是否可以在此处禁用useEffect

为了保留每个组件的非状态信息,有趣的是,您使用useRefdocs for useRef 指出它不仅适用于 DOM 元素引用,还适用于每个组件的非状态数据。所以你可以有

const firstRenderRef = useRef(true);

然后在您的useEffect 回调中,检查firstRenderRef.current&mndash;如果是true,则设置为false,否则进行滚动:

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>

作为一个思想实验,我写了一个钩子来简化人体工程学:

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

用法:

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

useEffect中,如果inst.first为真,则做inst.first = false;;否则,进行滚动。

直播:

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>

【讨论】:

  • 非常感谢,这行得通!你知道第一次渲染时是否可以在这里禁用useEffect
  • @R.Kohlisch - 我已添加到答案的末尾以展示如何做到这一点。
  • 这太棒了,非常感谢。我希望我能更多地支持这个答案!
  • Refs 更可靠地获取您关心的特定 DOM 节点。如果有多个元素与查询匹配,使用querySelector 或类似的东西(getElementsByClassgetElementById)可能会产生意想不到的结果,尤其是当元素之外的元素与之匹配时。如果您编写的组件会在很多地方多次出现,那么 refs 就更胜一筹了。
  • 此答案中的第 2 步(有条件地分配 ref)绝对是天才。几天来,我一直在使用自定义预输入,我只想为键盘事件调用一些自定义滚动逻辑,因为它会随着鼠标滚动而搞砸。因此,需要在父母中为孩子提供参考,这个解决方案效果很好。很棒的东西。
【解决方案2】:

添加已接受的答案并尝试回答问题的“应该”部分,使用 refs 进行 DOM 操作:

  • refs 可以更容易地在线性时间内唯一标识 + 选择相应的元素(与 id 相比,多个元素可能错误地具有相同的 + 值,而 document.querySelector 需要扫描 DOM 以选择正确的元素)
  • refs 知道 react 组件的生命周期,因此 react 会确保在卸载组件时将 refs 更新为 null 并提供更多开箱即用的便利。
  • refs 作为一个概念 + 语法与平台无关,因此您可以在 react native 和浏览器中使用相同的理解,而查询选择器是浏览器的事情
  • 对于没有 DOM 的 SSR,仍然可以使用 refs 来定位反应元素

当然,使用查询选择器并没有错,如果你在反应世界中使用它也不会破坏你的功能,但最好使用框架提供的东西,因为它在大多数情况下都有一些默认的好处.

【讨论】:

    猜你喜欢
    • 2017-02-09
    • 2019-01-07
    • 1970-01-01
    • 2018-07-29
    • 2018-11-29
    • 2020-08-10
    • 1970-01-01
    • 2020-10-31
    • 2017-11-04
    相关资源
    最近更新 更多