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


77

据我了解,我可以将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")
);
<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>

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

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

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


如果这很愚昧,请原谅我,但是如果您只调用useRef()一次,为什么还要期望元素具有不同的引用?AFAIK,React使用ref作为迭代元素的标识符,因此当您使用相同的ref时,它不知道它们之间的区别
MTCoster

2
因为我仍在学习钩子和裁判,所以这里不要无知。因此,任何建议对我来说都是不错的建议。这就是我想要做的,为不同的元素动态创建不同的引用。我的第二个示例只是“不要使用此示例” :)
devserkan

[1,2,3]来自哪里?它是静态的吗?答案取决于它。
Estus Flask,

最终,它们将来自远程端点。但是现在,如果我学习静态的,我会很高兴的。如果您能为偏僻的情况做出解释,那就太好了。谢谢。
devserkan '19

Answers:


64

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

在某个时候应该有一组引用。如果渲染之间数组长度可能有所不同,则数组应相应缩放:

  const arrLength = arr.length;
  const [elRefs, setElRefs] = React.useState([]);

  React.useEffect(() => {
    // add or remove refs
    setElRefs(elRefs => (
      Array(arrLength).fill().map((_, i) => elRefs[i] || createRef())
    ));
  }, [arrLength]);

  return (
    <div>
      {arr.map((el, i) => (
        <div ref={elRefs[i]} style={...}>...</div>
      ))}
    </div>
  );

可以通过解包useEffect和替换来优化这段代码useStateuseRef但是应该注意,在渲染函数中产生副作用通常被认为是一种不好的做法:

  const arrLength = arr.length;
  const elRefs = React.useRef([]);

  if (elRefs.current.length !== arrLength) {
    // add or remove refs
    elRefs.current = Array(arrLength).fill().map((_, i) => elRefs.current[i] || createRef())
    ));
  }

  return (
    <div>
      {arr.map((el, i) => (
        <div ref={elRefs.current[i]} style={...}>...</div>
      ))}
    </div>
  );

1
感谢您的回答@estus。这清楚地显示了我如何创建引用。请提供一种方法,如果可能的话,如何将这些引用与“ state”一起使用?由于在这种状态下,如果我没记错的话,我将无法使用任何引用。它们不是在第一个渲染之前创建的,useEffect我猜测应该以某种方式使用和声明它们。假设,我想像第一个示例中一样使用refs来获得那些元素的宽度。
devserkan

我不确定我是否正确理解您。但是,一个州也可能需要一个数组,例如setElWidth(elRef.current.map(innerElRef => innerElRef.current.offsetWidth)]
Estus Flask

3
它仅在数组始终具有相同长度时才有效,如果长度不同,则解决方案将不起作用。
奥利维尔·布瓦西(OlivierBoissé)

1
@OlivierBoissé在上面的代码中,这将在内部发生.map((el, i) => ...
Estus Flask,

1
@Greg好处是在渲染功能中没有副作用,这被认为是可以接受的不良做法,但不应作为经验法则推荐。如果为了进行初步优化而采用相反的方法,那也将是批评答案的原因。我想不出一个案例,它会使原位副作用在这里真的是一个糟糕的选择,但这并不意味着它不存在。我将保留所有选项。
Estus Flask

95

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

我想数组来自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>
    ));
}

引用大小未知的项目数组。
MeanMan '19

12
优秀的!额外的注释,在TypeScript中,签名itemsRef似乎是: const itemsRef = useRef<Array<HTMLDivElement | null>>([])
Mike Niebling,

1
您可以通过在构造函数中创建实例变量来在类组件中获得相同的结果this.itemsRef = []。然后,您需要在componentDidUpdate生命周期方法内移动useEffect代码。最后的render方法,你应该使用<div key={i} ref={el => this.itemsRef.current [i] = EL}`存储裁判
奥利维尔Boissé

2
这对我来说不算什么。
Vinay Prajapati

1
如果期望的数组可能更大,这将如何工作?
madav '19

16

有两种方法

  1. 将一个参考与多个当前元素一起使用
const inputRef = useRef([]);

inputRef.current[idx].focus();

<input
  ref={el => inputRef.current[idx] = el}
/>

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

  1. 使用引用数组

    正如上面的帖子所述,由于官方指南(以及内部的棉绒检查)不允许其通过,因此不建议这样做。

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

    但是,由于这不是我们当前的情况,因此下面的演示仍然有效,仅不建议这样做。

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

inputRef[idx].current.focus();

<input
  ref={inputRef[idx]}
/>


选项2是我尝试showCallout()在react-native-maps上使用Marker的方法
-7ahid

简单但有帮助
Faris Rayhan

4

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

该文件说

不要在循环,条件或嵌套函数中调用Hook。相反,请始终在React函数的顶层使用Hook。通过遵循此规则,可以确保每次渲染组件时都以相同的顺序调用Hook。这就是让React在多个useState和useEffect调用之间正确保留Hook的状态的原因。(如果您感到好奇,我们将在下面对此进行深入说明。)

但是请考虑一下,它显然适用于动态数组...但是,如果您使用的是静态数组(总是渲染相同数量的组件),则不必为此担心太多,要知道自己在做什么并利用它😉


2

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

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

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

}

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


为什么,如果您已经在检查el是否在数组中,是否应该在每个渲染周期将其清空?
GWorking'Mar

因为每个渲染周期都会将其添加到数组中,所以我们只需要el的一个副本。

1
是的,但您不检查!refs.current.includes(el)吗?
GWorking

2

假设您的数组包含非基本元素,则可以使用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>
    )
}

实际上,在我的实际情况下,我的数组包含非基本体,但是我不得不遍历该数组。我认为WeakMap是不可能的,但是如果不需要迭代,那确实是个不错的选择。谢谢。PS:啊,有一个建议,现在是第3阶段。很
高兴

0

我们不能使用状态,因为我们需要在调用render方法之前使ref可用。我们不能任意调用useRef,但是可以调用一次:

假设arr是一堆东西的道具:

const refs = useRef([]);
// free any refs that we're not using anymore
refs.current = refs.current.slice(0, arr.length);
// initialize any new refs
for (let step = refs.current.length; step < arr.length; step++) {
    refs.current[step] = createRef();
}

参考文件应该有副作用,例如useEffect()...avoid setting refs during rendering — this can lead to surprising behavior. Instead, typically you want to modify refs in event handlers and effects. reactjs.org/docs/...
丹尼尔·奥兰多

0

如果我理解正确,useEffect则仅应用于副作用,因此我选择使用useMemo

const App = props => {
    const itemsRef = useMemo(() => Array(props.items.length).fill().map(() => createRef()), [props.items]);

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

然后,如果您想操纵物品/使用副作用,则可以执行以下操作:

useEffect(() => {
    itemsRef.map(e => e.current).forEach((e, i) => { ... });
}, [itemsRef.length])
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.