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

IT技术 javascript reactjs react-hooks
2021-02-09 21:34:19

据我所知,我可以将 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>

我已经看到了这个,因此这个但是,我仍然对如何为这个简单案例实施该建议感到困惑。

6个回答

由于您 不能在 loops 内使用钩子,这里有一个解决方案,以便在数组随时间变化时使其工作。

我想数组来自 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>
    ));
}
这不适合我。
2021-03-17 21:34:19
refs 用于事先未知大小的项目数组。
2021-03-20 21:34:19
通过在构造函数中创建一个实例变量,您可以在类组件中获得相同的结果this.itemsRef = []然后您需要将 useEffect 代码移动到 componentDidUpdate 生命周期方法中。最后在该render方法中,您应该使用<div key={i} ref={el => this.itemsRef.current[i] = el} ` 来存储引用
2021-03-24 21:34:19
优秀的!额外的注意事项,在 TypeScript 中的签名itemsRef似乎是: const itemsRef = useRef<Array<HTMLDivElement | null>>([])
2021-03-26 21:34:19
如果预期的数组可能更大,这如何工作?
2021-04-10 21:34:19

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

在某些时候应该有一组 refs。如果阵列长度可能因渲染而异,则阵列应相应地缩放:

  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>
  );
它仅在数组始终具有相同长度时才有效,如果长度变化,您的解决方案将不起作用。
2021-03-19 21:34:19
@Greg 好处是在渲染函数中没有副作用,这被认为是一种不好的做法,可以接受但不应该作为经验法则被推荐。如果我为了初步优化而反其道而行之,那也是批评答案的理由。我想不出一个案例会使就地副作用成为一个非常糟糕的选择,但这并不意味着它不存在。我会留下所有的选择。
2021-03-19 21:34:19
感谢您的回答@estus。这清楚地显示了我如何创建参考。如果可能,您能否提供一种方法,我如何将这些引用与“状态”一起使用?因为在这种状态下,如果我没记错的话,我不能使用任何 refs。它们不是在第一次渲染之前创建的,useEffect我想不知何故我需要使用和说明。假设,我想像我在第一个示例中所做的那样使用 refs 获取这些元素的宽度。
2021-03-29 21:34:19
我不确定我是否正确理解你。但是一个状态也可能需要是一个数组,比如setElWidth(elRef.current.map(innerElRef => innerElRef.current.offsetWidth)]
2021-04-01 21:34:19
@OlivierBoissé 在上面的代码中,这会发生在.map((el, i) => ....
2021-04-03 21:34:19

有两种方式

  1. 对多个当前元素使用一个ref
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. 使用ref数组

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

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

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

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

inputRef[idx].current.focus();

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

简单但有帮助
2021-03-18 21:34:19
选项#2 不正确。你只能在顶层使用钩子:pl.reactjs.org/docs/... #2 对你有用,只要列表的长度是恒定的,但是当你向列表中添加一个新项目时,它会抛出一个错误。
2021-04-04 21:34:19
选项二是我尝试showCallout()在 react-native-maps 上使用Marker的方法
2021-04-05 21:34:19
@Adrian 正如我在答案中所说的那样,不允许这样写,也不推荐,您可以选择不使用它并单击downvote,但它不会让上面的演示不起作用(您可以尝试也可以通过单击show code snippet然后Run)。我仍然保留#2 的原因是为了更清楚为什么存在这个问题。
2021-04-05 21:34:19
第一种方法就像魅力一样。
2021-04-06 21:34:19

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

文档说

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

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

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

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

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

是的,但你不检查!refs.current.includes(el)吗?
2021-03-17 21:34:19
因为每个渲染周期它都会将它添加到数组中,所以我们只需要 el 的一个副本。
2021-03-25 21:34:19
为什么,如果你已经在检查 el 是否在数组中,你应该在每个渲染周期清空它吗?
2021-04-09 21:34:19