据我所知,我可以像这样为单个元素使用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>

我见过这个,所以才会这样。但是,我仍然不知道如何在这个简单的案例中实现这个建议。


当前回答

我们可以使用数组ref来记住ref列表:

import { RefObject, useRef } from 'react';

type RefObjects<T> = RefObject<T>[];

function convertLengthToRefs<T>(
  length: number,
  initialValue: T | null,
): RefObjects<T> {
  return Array.from(new Array(length)).map<RefObject<T>>(() => ({
    current: initialValue,
  }));
}

export function useRefs<T>(length: number, initialValue: T | null = null) {
  const refs = useRef<RefObjects<T>>(convertLengthToRefs(length, initialValue));

  return refs.current;
}

这是一个演示:

const dataList = [1, 2, 3, 4];

const Component: React.FC = () => {
  const refs = useRefs<HTMLLIElement>(dataList.length, null);

  useEffect(() => {
    refs.forEach((item) => {
      console.log(item.current?.getBoundingClientRect());
    });
  }, []);

  return (
    <ul>
      {dataList.map((item, index) => (
        <li key={item} ref={refs[index]}>
          {item}
        </li>
      ))}
    </ul>
  );
};


其他回答

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

在某些时候应该有一个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并将useState替换为useRef来优化,但应该注意的是,在呈现函数中执行副作用通常被认为是一种糟糕的做法:

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>
);
import React, { useRef } from "react";

export default function App() {
  const arr = [1, 2, 3];

  const refs = useRef([]);

  return (
    <div className="App">
      {arr.map((item, index) => {
        return (
          <div
            key={index}
            ref={(element) => {
              refs.current[index] = element;
            }}
          >
            {item}
          </div>
        );
      })}
    </div>
  );
}

学分:https://eliaslog.pw/how-to-add-multiple-refs-to-one-useref-hook/

通过将子元素移动到单独的组件中,可以避免数组引用与useEffect结合带来的复杂性。这还有其他优点,主要是可读性强,易于维护。

const { useRef, useState, useEffect } = React;

const ListComponent = ({ el }) => {
  const elRef = useRef();
  const [elWidth, setElWidth] = useState();

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

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

const App = () => {

  return (
    <div>
      {[1, 2, 3].map((el, i) => (
        <ListComponent key={i} el={el} />
      ))}
    </div>
  );
};

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

以上所有其他选项都依赖于数组,但这使得事情非常脆弱,因为元素可能会重新排序,然后我们就不跟踪ref属于哪个元素了。

React使用键道具来跟踪项目。因此,如果你按键存储你的引用,就不会有任何问题:

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

  const setRefFromKey = (key: string) => (element: HTMLElement | null) => {
    refs.current[key] = element;
  }

  return {refs: refs.current, setRefFromKey};
}
const Comp = ({ items }) => {
  const {refs, setRefFromKey} = useRefs()

  const refsArr = Object.values(refs) // your array of refs here

  return (
    <div>
      {items.map(item => (
        <div key={item.id} ref={setRefFromKey(item.id)}/>
      )}
    </div>
  )
}

请注意,React在卸载一个项目时,将使用null调用所提供的函数,该函数将在对象中将匹配的键项设置为null,因此所有内容都将是最新的。

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

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

Demo

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