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

时间:2019-02-11 15:18:47

标签: 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")
);
<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>

我见过this,因此也见过this。但是,对于这种简单情况,我仍然感到困惑。

9 个答案:

答案 0 :(得分:15)

有两种方法

  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

    数组

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

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

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

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

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

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

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

答案 2 :(得分:2)

假设您的数组包含非原语,则可以使用WeakMap作为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>
    )
}

答案 3 :(得分:1)

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

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

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

答案 4 :(得分:0)

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

文档说

  

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

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

答案 5 :(得分:0)

cannot use hooks inside loops来了,这是一个解决方案,可以在阵列随时间变化时使其工作。

我想数组来自道具:

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

答案 6 :(得分: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();
}

答案 7 :(得分: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])

答案 8 :(得分:0)

最简单和最有效的方法是根本不使用 useRef。只需使用 回调引用,它会在每次渲染时创建一个新的引用数组。

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

演示

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

<script type="text/babel" defer>
const { useEffect, useRef, 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>