如何在使用react useRef时针对DOM进行定位?

61

我正在寻找一种关于如何使用React的useRef()钩子获取DOM元素数组的解决方案。

例子:

const Component = () => 
{

  // In `items`, I would like to get an array of DOM element
  let items = useRef(null);

  return <ul>
    {['left', 'right'].map((el, i) =>
      <li key={i} ref={items} children={el} />
    )}
  </ul>
}

我该怎样实现这个目标?

8个回答

83

useRef只是部分类似于React的ref(仅具有current字段的对象结构)。

useRef钩子旨在在渲染之间存储一些数据,并且更改该数据不会触发重新渲染(与useState不同)。

还要温馨提示:最好避免在循环或if中初始化钩子。这是hooks的第一条规则

有了这个想法,我们:

  1. create array and keep it between renders by useRef

  2. we initialize each array's element by createRef()

  3. we can refer to list by using .current notation

    const Component = () => {
    
      let refs = useRef([React.createRef(), React.createRef()]);
    
      useEffect(() => {
        refs.current[0].current.focus()
      }, []);
    
      return (<ul>
        {['left', 'right'].map((el, i) =>
          <li key={i}><input ref={refs.current[i]} value={el} /></li>
        )}
      </ul>)
    }
    
这样我们就可以安全地修改数组(比如通过改变长度)。但是不要忘记,修改被存储在useRef中的数据不会触发重新渲染。因此,为了使改变长度重新渲染,我们需要使用useState
const Component = () => {

  const [length, setLength] = useState(2);
  const refs = useRef([React.createRef(), React.createRef()]);

  function updateLength({ target: { value }}) {
    setLength(value);
    refs.current = refs.current.splice(0, value);
    for(let i = 0; i< value; i++) {
      refs.current[i] = refs.current[i] || React.createRef();
    }
    refs.current = refs.current.map((item) => item || React.createRef());
  }

  useEffect(() => {
   refs.current[refs.current.length - 1].current.focus()
  }, [length]);

  return (<>
    <ul>
    {refs.current.map((el, i) =>
      <li key={i}><input ref={refs.current[i]} value={i} /></li>
    )}
  </ul>
  <input value={refs.current.length} type="number" onChange={updateLength} />
  </>)
}

同时,不要在首次渲染时尝试访问refs.current[0].current - 这将引发错误。

请注意:

      return (<ul>
        {['left', 'right'].map((el, i) =>
          <li key={i}>
            <input ref={refs.current[i]} value={el} />
            {refs.current[i].current.value}</li> // cannot read property `value` of undefined
        )}
      </ul>)

所以你要么将其作为{{保护}}

      return (<ul>
        {['left', 'right'].map((el, i) =>
          <li key={i}>
            <input ref={refs.current[i]} value={el} />
            {refs.current[i].current && refs.current[i].current.value}</li> // cannot read property `value` of undefined
        )}
      </ul>)

或在useEffect钩子中访问它。原因:ref在元素呈现后绑定,因此在第一次运行渲染时尚未初始化。


13
您是一个老板,非常感谢您将这些内容解释得如此清楚!useRef(elts.map(React.createRef)) 恰好是我一直在寻找的,可以使函数组件不会丢掉我的引用。 - ohsully
美丽 - Alex Cory
2
非常感谢您的解释,@skyboyer。我已经使用以下方式使其动态化-const refs = useRef( new Array(2).fill(React.createRef()));。这会引起问题吗? - Shofol
1
有人知道如何在TypeScript版本中实现这个吗? - mr.bjerre
4
@Shofol 的问题在于它会创建一个单一的引用,然后用它来填充 useRef 数组。因此,如果您尝试引用其中任何一个项目,它们都将引用同一个实体。我发现 const refs = useRef([...new Array(3)].map(() => React.createRef())); 是一个更好的选择。 - SNYDERHAUS
显示剩余3条评论

16

我稍微扩展一下skyboyer的答案。为了性能优化(并避免潜在的奇怪错误),您可能更喜欢使用useMemo而不是useRef。因为useMemo接受一个回调作为参数而不是一个值,所以React.createRef仅在第一次渲染后初始化一次。在回调函数内,您可以返回一个createRef值的数组,并适当地使用该数组。

初始化:

  const refs= useMemo(
    () => Array.from({ length: 3 }).map(() => createRef()),
    []
  );

这里的空数组(作为第二个参数)告诉React仅初始化引用一次。如果引用计数发生变化,则可能需要将[x.length]作为“依赖项数组”传递,并动态创建引用:Array.from({ length: x.length }).map(() => createRef()),

用法:

  refs[i+1 % 3].current.focus();

3
дёәд»Җд№ҲuseMemoжӣҙеҸ—жҺЁиҚҗд»ҘеҸҠдҪ жүҖжҢҮзҡ„bugжҳҜе“ӘдәӣпјҢиҜ·жӮЁжҸҗдҫӣдёҖдәӣиҜҰз»ҶдҝЎжҒҜгҖӮ - skyboyer
3
警告:不要在 useEffect(...)、useMemo(...) 或其他内置 Hook 中调用 Hook。 - kidroca

12
获取父级引用并操作其子元素

const Component = () => {
  const ulRef = useRef(null);

  useEffect(() => {
    ulRef.current.children[0].focus();
  }, []);

  return (
    <ul ref={ulRef}>
      {['left', 'right'].map((el, i) => (
        <li key={i}>
          <input value={el} />
        </li>
      ))}
    </ul>
  );
};

我这样做,认为这比其他提出的答案更简单。


有趣的想法 - AGrush
您是个天才,非常感谢您提供如此聪明简单的答案。 - Let

5

不需要使用引用数组或类似的东西,您可以将每个地图项目分开到组件中。 当您将它们分开时,可以独立使用useRef

const DATA = [
  { id: 0, name: "John" },
  { id: 1, name: "Doe" }
];

//using array of refs or something like that:
function Component() {
  const items = useRef(Array(DATA.length).fill(createRef()));
  return (
    <ul>
      {DATA.map((item, i) => (
        <li key={item.id} ref={items[i]}>
          {item.name}
        </li>
      ))}
    </ul>
  );
}


//seperate each map item to component:
function Component() {
  return (
    <ul>
      {DATA.map((item, i) => (
        <MapItemComponent key={item.id} data={item}/>
      ))}
    </ul>
  );
}

function MapItemComponent({data}){
  const itemRef = useRef();
  return <li ref={itemRef}>
    {data.name}
  </li>
}


添加了@StephenOstermiller - tufanlodos
谢谢更新,看起来好多了。 - Stephen Ostermiller

2

如果您事先知道数组的长度,就像您在示例中所做的那样,您可以简单地创建一个引用数组,然后按其索引分配每个引用:

最初的回答

const Component = () => {
  const items = Array.from({length: 2}, a => useRef(null));
  return (
    <ul>
      {['left', 'right'].map((el, i)) => (
        <li key={el} ref={items[i]}>{el}</li>
      )}
    </ul>
  )
}

4
为什么会这样?React Hook的"useRef"不能在回调函数中被调用。React Hook必须在React函数组件或自定义React Hook函数中被调用。 - Liren Yeo
2
由于Hooks规则,特别是React依赖于调用Hooks的顺序。在回调中调用Hook会导致不稳定的调用序列。@LirenYeo - Matt Mc
2
const items = useRef(Array.from({length: 2}, () => React.createRef())) - skyboyer
这对任何人都有效吗? - Isaac Pak

1
我曾经遇到过这样的问题,看了“Joer”的回答后意识到可以通过循环使用索引来动态设置querySelector类,并仅将一个ref设置为整个父级。对于代码的负担表示歉意,但希望这能帮助到某些人:
import React, { useRef, useState } from 'react';
import { connectToDatabase } from "../util/mongodb";

export default function Top({ posts }) {
  //const [count, setCount] = useState(1);
  const wrapperRef = useRef(null);


  const copyToClipboard = (index, areaNumber) => {
    // 
    // HERE I AM USING A DYNAMIC CLASS FOR THE WRAPPER REF 
    // AND DYNAMIC QUERY SELECTOR, THEREBY ONLY NEEDING ONE REF ON THE TOP PARENT
    const onePost = wrapperRef.current.querySelector(`.index_${index}`)
    const oneLang = onePost.querySelectorAll('textarea')[areaNumber];
    oneLang.select();
    document.execCommand('copy');
  };

  var allPosts = posts.map((post, index) => {

    var formattedDate = post.date.replace(/T/, ' \xa0\xa0\xa0').split(".")[0]
    var englishHtml = post.en1 + post.en2 + post.en3 + post.en4 + post.en5;
    var frenchHtml = post.fr1 + post.fr2 + post.fr3 + post.fr4 + post.fr5;
    var germanHtml = post.de1 + post.de2 + post.de3 + post.de4 + post.de5;

    return (
      <div className={post.title} key={post._id}>
        <h2>{formattedDate}</h2>
        <h2>{index}</h2>

        <div className={"wrapper index_" + index}>
          <div className="one en">
            <h3>Eng</h3>
            <button onClick={() => {copyToClipboard(index, 0)}}>COPY</button>
            <textarea value={englishHtml} readOnly></textarea>
          </div>

          <div className="one fr">
            <h3>Fr</h3>
            <button onClick={() => {copyToClipboard(index, 1)}}>COPY</button> 
            <textarea value={frenchHtml} readOnly></textarea>
          </div>

          <div className="one de">
            <h3>De</h3>
            <button onClick={() => {copyToClipboard(index, 2)}}>COPY</button>
            <textarea value={germanHtml} readOnly></textarea>
          </div>
        </div>

      </div>
    )
  })

  return (
    <div ref={wrapperRef}>
      <h1>Latest delivery pages </h1>
      <ul>
        {allPosts}
      </ul>

      <style jsx global>{`

        body{
          margin: 0;
          padding: 0;
        }
        h1{
          padding-left: 40px;
          color: grey;
          font-family: system-ui;
          font-variant: all-small-caps;
        }
        .one,.one textarea {
          font-size: 5px;
          height: 200px;
          width: 300px;
          max-width:  350px;
          list-style-type:none;
          padding-inline-start: 0px;
          margin-right: 50px;
          margin-bottom: 150px;
          
        }

        h2{
          font-family: system-ui;
          font-variant: all-small-caps;
        }
        .one h3 {
          font-size: 25px;
          margin-top: 0;
          margin-bottom: 10px;
          font-family: system-ui;
        }

        .one button{
          width: 300px;
          height: 40px;
          margin-bottom: 10px;
        }

        @media screen and (min-width: 768px){
          .wrapper{
            display: flex;
            flex-direction: row;
          }
        }

      `}</style>
    </div>
  );
}

-1
在这种情况下,您需要创建一个空引用数组,并在生成React组件内的引用时推送引用。
然后,您将使用useEffect来处理相应的引用。
以下是一个示例:
const refs = []

useEffect(() => {
    refs.map(ref => {
      console.log(ref.current)
      // DO SOMETHING WITH ref.current
    })
  }, [refs])

{postsData.map((post, i) => {
   refs.push(React.createRef())
   return (
     <div ref={refs[i]} key={post.id}>{...}</div>
   )}
}

-1
我知道有点晚了,但我发现了一种使用.childNodes的解决方案,我觉得非常巧妙。我不确定它是否适用于你的用例,但对我来说完美无缺:
    const navList = [
        { title: 'Add an event', link: '/createEvent' },
        { title: 'Contact me', link: '/contact' },
        { title: 'About this site', link: '/about' },
    ];

    // useRef to access DOM elements
    const navBarRef = useRef<HTMLUListElement>(null);
    const navItemRef = useRef<HTMLLIElement>(null);

    // map navList to create navItems
    const navItemsMapped = (
        <ul className={styles.navBar} ref={navBarRef}>
            {navList.map((item, index) => {
                return (
                    // note ref assigned to each li element
                    <li key={index} className={styles.navItem} ref={navItemRef}>
                        <Link href={item.link}>{item.title}</Link>
                    </li>
                );
            })}
        </ul>
    );


    const navButtonClickHandler = () => {
        // navBar styles
        const navBar = navBarRef.current;
        navBar?.classList.toggle(styles.navBarActive);
        // navItem styles
        const navItems = navBar?.childNodes;
        // dynamically assign ref to each navItem based on their status as childNodes of navBar, rather than using a static ref
        navItems?.forEach((item) => {
            const itemRef = item as HTMLLIElement;
            itemRef.classList.toggle(styles.navItemActive);
        });
    };

关键在于最后的 forEach,其中每个引用都是基于 navItem 状态作为 navBar 的子节点来分配的,而不是静态分配。我还应该注意到这是 NextJS/TypeScript 的摘录。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接