【问题标题】:How can I use multiple refs for an array of elements with hooks?如何为带有钩子的元素数组使用多个引用?
【发布时间】:2019-07-05 03:10:27
【问题描述】:

据我了解,我可以将 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。但是,我仍然对如何为这个简单案例实施该建议感到困惑。

【问题讨论】:

  • 如果这是无知的,请原谅我,但如果你只调用一次useRef(),你为什么期望元素有不同的引用? AFAIK,React 使用 ref 作为迭代元素的标识符,所以当你使用相同的 ref 时它不知道它们之间的区别
  • 这里没有无知,因为我还在学习 hooks 和 refs。所以任何建议对我来说都是很好的建议。这就是我想做的,为不同的元素动态创建不同的参考。我的第二个例子只是“不要使用这个”例子:)
  • [1,2,3] 是从哪里来的?是静态的吗?答案取决于它。
  • 最终,它们将来自远程端点。但是现在,如果我学会了静态的,我会很高兴的。如果你能解释偏远的情况,那就太棒了。谢谢。

标签: javascript reactjs react-hooks


【解决方案1】:

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

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

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

【讨论】:

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

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

文档说

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

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

【讨论】:

    【解决方案3】:

    正如您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>
        ));
    }
    

    【讨论】:

    • 引用一个事先不知道大小的项目数组。
    • 太棒了!额外说明,在 TypeScript 中,itemsRef 的签名似乎是:const itemsRef = useRef&lt;Array&lt;HTMLDivElement | null&gt;&gt;([])
    • 您可以通过在构造函数中使用this.itemsRef = [] 创建实例变量来在类组件中获得相同的结果。然后你需要在 componentDidUpdate 生命周期方法中移动 useEffect 代码。最后在render 方法中,您应该使用&lt;div key={i} ref={el =&gt; this.itemsRef.current[i] = el} ` 来存储参考
    • 这不适合我。
    • 如果预期的数组可能更大,这将如何工作?
    【解决方案4】:

    您可以使用数组(或对象)来跟踪所有 ref 并使用方法将 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

    【讨论】:

    • 为什么,如果你已经在检查 el 是否在数组中,你应该在每个渲染周期清空它吗?
    • 因为每个渲染周期都会将它添加到数组中,所以我们只需要一个 el 副本。
    • 是的,但你不是在检查!refs.current.includes(el)吗?
    【解决方案5】:

    我们不能使用状态,因为我们需要在调用渲染方法之前 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/…
    【解决方案6】:

    有两种方法

    1. 使用一个 ref 和多个 current 元素
    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 检查)不允许它通过。

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

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

    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>

    【讨论】:

    • 选项二是我尝试在 react-native-maps Markers 上使用 showCallout() 的方法
    • 简单但有用
    • 选项 #2 不正确。您只能在顶层使用钩子:pl.reactjs.org/docs/… #2 只要列表的长度不变,就可以为您工作,但是当您将新项目添加到列表中时,它会引发错误。
    • @Adrian 正如我在回答中所说的那样,不允许这样写,也不推荐,你可以选择不使用它并点击downvote,但它不会让上面的演示不起作用(您也可以点击show code snippet 然后点击Run 尝试)。我仍然保留 #2 的原因是为了更清楚地说明为什么存在这个问题。
    • 第一种方法很有效。
    【解决方案7】:

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

    【讨论】:

    • 实际上,在我的真实案例中,我的数组包含非基元,但我必须遍历数组。我认为 WeakMap 不可能,但如果不需要迭代,它确实是一个不错的选择。谢谢。 PS:啊,有一个proposal,现在是第3阶段。很高兴知道:)
    • 我是react / js新手,很抱歉我的幼稚,但是ref属性有回调函数?另外,如果不使用stackoverflow,如何了解这些信息?有我可以使用的文档/手册吗?谢谢
    【解决方案8】:

    如果我理解正确,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])
    

    【讨论】:

      【解决方案9】:

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

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

      演示

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

      【讨论】:

        【解决方案10】:

        React 将在其 ref 更改时重新渲染一个元素(引用相等/“三相等”检查)。

        这里的大多数答案都没有考虑到这一点。更糟糕的是:当父级渲染并重新初始化 ref 对象时,所有子级都会重新渲染,even if they are memoized componentsReact.PureComponentReact.memo)!

        下面的解决方案没有不必要的重新渲染,适用于动态列表,甚至没有引入实际的副作用。无法访问未定义的 ref。 ref 在第一次读取时被初始化。之后,它保持引用稳定。

        const useGetRef = () => {
          const refs = React.useRef({})
          return React.useCallback(
            (idx) => (refs.current[idx] ??= React.createRef()),
            [refs]
          )
        }
        
        const Foo = ({ items }) => {
          const getRef = useGetRef()
          return items.map((item, i) => (
            <div ref={getRef(i)} key={item.id}>
              {/* alternatively, to access refs by id: `getRef(item.id)` */}
              {item.title}
            </div>
          ))
        }
        

        警告:items 随时间缩小时,未使用的 ref 对象将不会被清理。当 React 卸载一个元素时,它会正确设置 ref[i].current = null,但“空”的 refs 会保留。

        【讨论】:

          【解决方案11】:

          我使用 useRef 挂钩来创建我想要独立控制的数据面板。首先我初始化 useRef 来存储一个数组:

          import React, { useRef } from "react";
          
          const arr = [1, 2, 3];
          
          const refs = useRef([])
          

          在初始化数组时,我们观察到它实际上看起来像这样:

          //refs = {current: []}
          

          然后我们使用 map 函数来创建面板,使用我们将要引用的 div 标签,将当前元素添加到我们的 refs.current 数组中,一键查看:

          arr.map((item, index) => {
            <div key={index} ref={(element) => {refs.current[index] = element}}>
              {item}
              <a
                href="#"
                onClick={(e) => {
                  e.preventDefault();
                  onClick(index)
                }}
              >
                Review
              </a>
          })
          

          最后一个函数接收按下按钮的索引,我们可以控制我们想要显示的面板

          const onClick = (index) => {
            console.log(index)
            console.log(refs.current[index])
          }
          

          最后完整的代码应该是这样的

          import React, { useRef } from "react";
          
          const arr = [1, 2, 3];
          
          const refs = useRef([])
          //refs = {current: []}
          
          const onClick = (index) => {
            console.log(index)
            console.log(refs.current[index])
          }
          
          const MyPage = () => {
             const content = arr.map((item, index) => {
               <div key={index} ref={(element) => {refs.current[index] = element}}>
                 {item}
                 <a
                   href="#"
                   onClick={(e) => {
                     e.preventDefault();
                     onClick(index)
                   }}
                 >
                   Review
                 </a>
             })
             return content
          }
          
          export default MyPage
          

          它对我有用!希望这些知识对你有用。

          【讨论】:

            猜你喜欢
            • 2022-01-17
            • 1970-01-01
            • 2019-09-28
            • 2020-11-27
            • 2011-07-06
            • 2012-05-16
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多