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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/54633690/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-23 05:14:06  来源:igfitidea点击:

How can I use multiple refs for an array of elements with hooks?

javascriptreactjsreact-hooks

提问by devserkan

As far as I understood I can use refs for a single element like this:

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

How can I implement this for an array of elements? Obviously not like that: (I knew it even I did not try it:)

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

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>

I have seen thisand hence this. But, I'm still confused about how to implement that suggestion for this simple case.

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

采纳答案by Estus Flask

A ref is initially just { current: null }object. useRefkeeps the reference to this object between component renders. currentvalue is primarily intended for component refs but can hold anything.

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

There should be an array of refs at some point. In case array length may vary between renders, an array should scale accordingly:

在某些时候应该有一组 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>
  );

This piece of code can be optimized by unwrapping useEffectand replacing useStatewith useRefbut it should be noted that doing side effects in render function is generally considered a bad practice:

这段代码可以通过解包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>
  );

回答by Olivier Boissé

As you cannot use hooks inside loops, here is a solution in order to make it work when the array changes over the time.

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

I suppose the array comes from the props :

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

回答by NoriSte

Note that you shouldn't use useRef in a loop for a simple reason: the order of used hooks does matter!

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

The documentation says

文档说

Don't call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That's what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls. (If you're curious, we'll explain this in depth below.)

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

But consider that it obviously applies to dynamic arrays... but if you're using static arrays (you ALWAYS render the same amount of components) don't worry too much about that, be aware of what you're doing and leverage it

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

回答by keikai

There are two ways

有两种方式

  1. use an Array of ref
  1. 使用ref数组
const inputRef = list.map(x => useRef(null));

inputRef[idx].current.focus();

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

const {useRef} = React;
const App = () => {
const list = [...Array(8).keys()];
const inputRef = list.map(x => useRef(null));
const handler = idx => () => {
  const next = inputRef[idx + 1];
  if (next) {
    next.current.focus();
  }
};
return (
  <div className="App">
    <div className="input_boxes">
      {list.map(x => (
      <div>
        <input
          key={x}
          ref={inputRef[x]}
          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. use one refwith multiple currentelements
  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>

回答by Neo

You can use an array(or an object) to keep track of all the refs and use a method to add ref to the array.

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

NOTE: If you are adding and removing refs you would have to empty the array every render cycle.

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

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

}

working example https://codesandbox.io/s/serene-hermann-kqpsu

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

回答by Greg

We can't use state because we need the ref to be available before the render method is called. We can't call useRef an arbitrary number of times, but we can call it once:

我们不能使用 state,因为我们需要在调用 render 方法之前 ref 可用。我们不能随意调用 useRef 多次,但我们可以调用它一次:

Assuming arris a prop with the array of things:

假设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();
}