目标DOM如何在地图中反应useRef

时间:2019-03-01 08:08:04

标签: javascript reactjs react-hooks

我正在寻找一种解决方案,以获取带有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>
}

我该如何实现?

6 个答案:

答案 0 :(得分:5)

获取父引用并操作子引用

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>
  );
};

我以这种方式工作,我认为这比其他建议的答案更简单。

答案 1 :(得分:3)

useRef仅与React的ref(仅具有current字段的对象的结构)部分相似。

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

也要提醒一下:最好避免在循环或if中初始化钩子。是first rule of hooks

记住这一点,我们:

  1. 创建数组并通过useRef将其保留在渲染之间
  2. 我们通过createRef()初始化每个数组的元素
  3. 我们可以使用.current表示法来引用列表

    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是在元素渲染后绑定的,因此在渲染第一次运行时尚未初始化。

答案 2 :(得分:3)

您可以将每个地图项与组件分开,而不是使用引用数组或类似的东西。当您将它们分开时,您可以单独使用 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>
}

答案 3 :(得分:2)

如果您提前知道数组的长度(在示例中将这样做),则可以简单地创建一个ref数组,然后按其索引分配每个引用:

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 :(得分:0)

我将在skyboyer's answer上进行扩展。为了优化性能(并避免潜在的怪异错误),您可能更喜欢使用useMemo而不是useRef。由于useMemo接受回调作为参数而不是值,因此React.createRef仅在第一次渲染后初始化一次。在回调内部,您可以返回一个createRef值数组,并适当地使用该数组。

初始化:

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

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

用法:

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

答案 5 :(得分:0)

我遇到了这样的问题并阅读了“乔尔的回答”并意识到您可以使用索引设置动态查询选择器类并仅将一个引用设置为整个父级。对代码负载深表歉意,但希望这对某人有所帮助:

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>
  );
}