如何对带有钩子的元素数组使用多个引用?

dev*_*kan 10 javascript reactjs react-hooks

据我了解,我可以将refs用于单个元素,如下所示:

const { useRef, useState, useEffect } = React;

const App = () => {
  const elRef = useRef();
  const [elWidth, setElWidth] = useState();

  useEffect(() => {
    setElWidth(elRef.current.offsetWidth);
  }, []);

  return (
    <div>
      <div ref={elRef} style={{ width: "100px" }}>
        Width is: {elWidth}
      </div>
    </div>
  );
};

ReactDOM.render(
  <App />,
  document.getElementById("root")
);
Run Code Online (Sandbox Code Playgroud)
<script src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>

<div id="root"></div>
Run Code Online (Sandbox Code Playgroud)

如何为元素数组实现此功能?显然不是这样:(即使我没有尝试过,我也知道:)

const { useRef, useState, useEffect } = React;

const App = () => {
  const elRef = useRef();
  const [elWidth, setElWidth] = useState();

  useEffect(() => {
    setElWidth(elRef.current.offsetWidth);
  }, []);

  return (
    <div>
      {[1, 2, 3].map(el => (
        <div ref={elRef} style={{ width: `${el * 100}px` }}>
          Width is: {elWidth}
        </div>
      ))}
    </div>
  );
};

ReactDOM.render(
  <App />,
  document.getElementById("root")
);
Run Code Online (Sandbox Code Playgroud)
<script src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>

<div id="root"></div>
Run Code Online (Sandbox Code Playgroud)

我已经看到了这个,因此这个。但是,对于这种简单情况,我仍然感到困惑。

kei*_*kai 26

有两种方式

  1. 对多个当前元素使用一个ref
const inputRef = useRef([]);

inputRef.current[idx].focus();

<input
  ref={el => inputRef.current[idx] = el}
/>
Run Code Online (Sandbox Code Playgroud)

const inputRef = useRef([]);

inputRef.current[idx].focus();

<input
  ref={el => inputRef.current[idx] = el}
/>
Run Code Online (Sandbox Code Playgroud)
const {useRef} = React;
const App = () => {
  const list = [...Array(8).keys()];
  const inputRef = useRef([]);
  const handler = idx => e => {
    const next = inputRef.current[idx + 1];
    if (next) {
      next.focus()
    }
  };
  return (
    <div className="App">
      <div className="input_boxes">
        {list.map(x => (
        <div>
          <input
            key={x}
            ref={el => inputRef.current[x] = el} 
            onChange={handler(x)}
            type="number"
            className="otp_box"
          />
        </div>
        ))}
      </div>
    </div>
  );
}
ReactDOM.render(<App />, document.getElementById("root"));
Run Code Online (Sandbox Code Playgroud)

  1. 使用ref数组

    正如上面的帖子所说,不推荐这样做,因为官方指南(和内部 lint 检查)不允许它通过。

    不要在循环、条件或嵌套函数中调用 Hook。相反,始终在 React 函数的顶层使用 Hook。通过遵循此规则,您可以确保每次渲染组件时以相同的顺序调用 Hook。

    但是,由于这不是我们当前的情况,因此下面的演示仍然有效,只是不推荐。

const inputRef = list.map(x => useRef(null));

inputRef[idx].current.focus();

<input
  ref={inputRef[idx]}
/>
Run Code Online (Sandbox Code Playgroud)

<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js"></script>
Run Code Online (Sandbox Code Playgroud)
const inputRef = list.map(x => useRef(null));

inputRef[idx].current.focus();

<input
  ref={inputRef[idx]}
/>
Run Code Online (Sandbox Code Playgroud)

  • 简单但有帮助 (3认同)

Emm*_*fon 18

上面的所有其他选项都依赖于数组,但它使事情变得极其脆弱,因为元素可能会被重新排序,然后我们不会跟踪哪个 ref 属于哪个元素。

React 使用keyprop 来跟踪项目。因此,如果您通过键存储引用,则不会有任何问题:

const useRefs = () => {
  const refsByKey = useRef<Record<string,HTMLElement | null>>({})

  const setRef = (element: HTMLElement | null, key: string) => {
    refsByKey.current[key] = element;
  }

  return {refsByKey: refsByKey.current, setRef};
}
Run Code Online (Sandbox Code Playgroud)
const Comp = ({ items }) => {
  const {refsByKey, setRef} = useRefs()

  const refs = Object.values(refsByKey).filter(Boolean) // your array of refs here

  return (
    <div>
      {items.map(item => (
        <div key={item.id} ref={element => setRef(element, item.id)}/>
      )}
    </div>
  )
}
Run Code Online (Sandbox Code Playgroud)

请注意,React 在卸载项目时,将调用提供的函数 with null,这会将对象中的匹配key条目设置为 null,因此所有内容都将是最新的。


Oli*_*ssé 15

由于您 不能在循环内使用钩子,因此有一种解决方案,可以使它在数组随时间变化时起作用。

我想数组来自props:

const App = props => {
    const itemsRef = useRef([]);
    // you can access the elements with itemsRef.current[n]

    useEffect(() => {
       itemsRef.current = itemsRef.current.slice(0, props.items.length);
    }, [props.items]);

    return props.items.map((item, i) => (
      <div 
          key={i} 
          ref={el => itemsRef.current[i] = el} 
          style={{ width: `${(i + 1) * 100}px` }}>
        ...
      </div>
    ));
}
Run Code Online (Sandbox Code Playgroud)

  • 出色的!额外注意的是,在 TypeScript 中 `itemsRef` 的签名似乎是: ```const itemsRef = useRef&lt;Array&lt;HTMLDivElement | 空&gt;&gt;([])``` (17认同)
  • 任何人都愿意解释使用“slice”背后的原因是什么,而不是简单地将引用重置为空数组 (4认同)
  • @Tanckom因为`useEffect`是在组件渲染之后调用的(因此在调用ref回调之后)。如果将 `itemsRef` 重置为 `useEffect` 内的空数组,那么您将丢失所有元素。 (4认同)
  • 通过在构造函数中使用“this.itemsRef = []”创建实例变量,您可以在类组件中获得相同的结果。然后,您需要将 useEffect 代码移至 componentDidUpdate 生命周期方法内。最后,在 `render` 方法中,您应该使用 `&lt;div key={i} ref={el =&gt; `this.itemsRef.current[i] = el} ` 来存储引用 (2认同)
  • 这对我不起作用。 (2认同)

Moh*_*oly 12

useRef最简单、最有效的方法就是根本不使用。只需使用回调引用即可在每次渲染时创建新的引用数组。

function useArrayRef() {
  const refs = []
  return [refs, el => el && refs.push(el)]
}
Run Code Online (Sandbox Code Playgroud)

演示

<div id="root"></div>

<script type="text/babel" defer>
const { useEffect, useState } = React

function useArrayRef() {
  const refs = []
  return [refs, el => el && refs.push(el)]
}

const App = () => {
  const [elements, ref] = useArrayRef()
  const [third, setThird] = useState(false)
  
  useEffect(() => {
    console.log(elements)
  }, [third])

  return (
    <div>
      <div ref={ref}>
        <button ref={ref} onClick={() => setThird(!third)}>toggle third div</button>
      </div>
      <div ref={ref}>another div</div>
      { third && <div ref={ref}>third div</div>}
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
</script>

<script src="https://unpkg.com/@babel/standalone@7/babel.min.js"></script>
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
Run Code Online (Sandbox Code Playgroud)

  • 如果您必须将其作为一个组件,那么这是一个很好的解决方案 (2认同)

Est*_*ask 11

ref最初只是{ current: null }对象。useRef在组件渲染之间保留对此对象的引用。currentvalue主要用于组件引用,但可以容纳任何内容。

在某个时候应该有一组引用。如果数组长度是固定的,则可以是:

  const elRef = useRef([...Array(3)].map(() => createRef()));
  ...  
  return (
    <div>
      {[1, 2, 3].map((el, i) => (
        <div ref={elRef.current[i]} style={{ width: `${el * 100}px` }}>...</div>
      ))}
    </div>
  );
Run Code Online (Sandbox Code Playgroud)

  • 它仅适用于数组始终具有相同长度的情况,如果长度不同,则您的解决方案将不起作用。 (5认同)

Neo*_*Neo 8

您可以使用数组(或对象)来跟踪所有引用,并使用方法将引用添加到数组中。

注意:如果要添加和删除引用,则必须在每个渲染周期清空数组。

import React, { useRef } from "react";

const MyComponent = () => {
   // intialize as en empty array
   const refs = useRefs([]); // or an {}
   // Make it empty at every render cycle as we will get the full list of it at the end of the render cycle
   refs.current = []; // or an {}

   // since it is an array we need to method to add the refs
   const addToRefs = el => {
     if (el && !refs.current.includes(el)) {
       refs.current.push(el);
     }
    };
    return (
     <div className="App">
       {[1,2,3,4].map(val => (
         <div key={val} ref={addToRefs}>
           {val}
         </div>
       ))}
     </div>
   );

}
Run Code Online (Sandbox Code Playgroud)

工作示例 https://codesandbox.io/s/serene-hermann-kqpsu


Jor*_*ela 8

我使用 useRef 挂钩来创建我想要独立控制的数据面板。首先,我初始化 useRef 来存储数组:

import React, { useRef } from "react";

const arr = [1, 2, 3];

const refs = useRef([])
Run Code Online (Sandbox Code Playgroud)

当初始化数组时,我们观察到它实际上是这样的:

//refs = {current: []}
Run Code Online (Sandbox Code Playgroud)

然后我们应用 map 函数来使用我们将引用的 div 标签创建面板,通过一键查看将当前元素添加到我们的 refs.current 数组中:

arr.map((item, index) => {
  <div key={index} ref={(element) => {refs.current[index] = element}}>
    {item}
    <a
      href="#"
      onClick={(e) => {
        e.preventDefault();
        onClick(index)
      }}
    >
      Review
    </a>
})
Run Code Online (Sandbox Code Playgroud)

最后一个接收按下按钮索引的函数我们可以控制我们想要显示的面板

const onClick = (index) => {
  console.log(index)
  console.log(refs.current[index])
}
Run Code Online (Sandbox Code Playgroud)

最后完整的代码是这样的

import React, { useRef } from "react";

const arr = [1, 2, 3];

const refs = useRef([])
//refs = {current: []}

const onClick = (index) => {
  console.log(index)
  console.log(refs.current[index])
}

const MyPage = () => {
   const content = arr.map((item, index) => {
     <div key={index} ref={(element) => {refs.current[index] = element}}>
       {item}
       <a
         href="#"
         onClick={(e) => {
           e.preventDefault();
           onClick(index)
         }}
       >
         Review
       </a>
   })
   return content
}

export default MyPage
Run Code Online (Sandbox Code Playgroud)

这个对我有用!希望这些知识对您有用。


Nor*_*Ste 7

请注意,出于一个简单的原因,您不应该在循环中使用 useRef:使用的钩子的顺序很重要!

文档说

不要在循环、条件或嵌套函数中调用 Hook。相反,始终在 React 函数的顶层使用 Hook。通过遵循此规则,您可以确保每次渲染组件时以相同的顺序调用 Hook。这就是允许 React 在多个 useState 和 useEffect 调用之间正确保留 Hooks 状态的原因。(如果你很好奇,我们将在下面深入解释这一点。)

但是考虑到它显然适用于动态数组......但是如果你使用静态数组(你总是渲染相同数量的组件)不要太担心,注意你在做什么并利用它


Dan*_* F. 7

假设您的数组包含非基元,您可以使用 aWeakMap作为 的值Ref

function MyComp(props) {
    const itemsRef = React.useRef(new WeakMap())

    // access an item's ref using itemsRef.get(someItem)

    render (
        <ul>
            {props.items.map(item => (
                <li ref={el => itemsRef.current.set(item, el)}>
                    {item.label}
                </li>
            )}
        </ul>
    )
}
Run Code Online (Sandbox Code Playgroud)