12

Using React, i have a list of ref statically declared this way:

  let line1 = useRef(null);
  let line2 = useRef(null);
  let line3 = useRef(null);
  ...
  //IN MY RENDER PART
  <li ref={(el) => (line1 = el)}>line1</li>
  <li ref={(el) => (line2 = el)}>line1</li>
  <li ref={(el) => (line3 = el)}>line1</li>

the refs are then passed to an animation function and everything works correctly; now things changed a bit and i create the list item using map and im no longer able to ref the element correctly; i tried something like:

{menu.menu.map((D) => {
let newRef = createRef();
                    LiRefs.push(newRef);
                    return (
                      <li
                        key={D.id}
                        ref={(el) => (newRef = el)} >
                        {D.label}
                      </li>
                    );
                  })}

but the array i pass to the animation function is empty (i guess because the function is called inside useEffect hook and LiRefs is not yet a useRef) i also know the number of

  • i will create, so i can declare them at the beginning and the reference with something like
    ref={(el) => (`line${i}` = el)}
    

    which is not working any other solution i could try?

  • Zach Saucier
    • 24,871
    • 12
    • 85
    • 147
    popeating
    • 386
    • 1
    • 2
    • 16
    • You probably want `React.useRef()` not `React.createRef()`. `createRef()` is only for use in React class components. – jered Dec 18 '20 at 00:49
    • 1
      However, calling _any_ hook inside of a loop is technically against the "rules of hooks". If you absolutely, positively, definitely know that the length of your "menu" array will not change, you can do it, but tread carefully. – jered Dec 18 '20 at 00:52
    • 1
      @jered `React.createRef` can be used anywhere you want/need to create a react ref, there is no rule I know of that says they can only be used in class-based components. If that was the case then surely the React [docs](https://reactjs.org/docs/refs-and-the-dom.html) would make that clear. – Drew Reese Dec 18 '20 at 00:57

    4 Answers4

    21

    Issue

    This won't work as each render when menu is mapped it creates new react refs.

    Solution

    Use a ref to hold an array of generated refs, and assign them when mapping.

    const lineRefs = React.useRef([]);
    
    lineRefs.current = menu.menu.map((_, i) => lineRefs.current[i] ?? createRef());
    

    later when mapping UI, attach the react ref stored in lineRefs at index i

    {menu.menu.map((D, i) => {
      return (
        <li
          key={D.id}
          ref={lineRefs.current[i]} // <-- attach stored ref
          {D.label}
        </li>
      );
    })}
    
    Drew Reese
    • 165,259
    • 14
    • 153
    • 181
    • complains about ?? is it && or || ? – Raul H Oct 19 '21 at 03:02
    • 2
      @RaulH Technically, neither, but the [Nullish Coalescing Operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator) (`??`) is more like logical OR (`||`) in this use case. The gist here is that if the left hand side (***LHS***) is specifically `null` or `undefined` the right hand side (***RHS***) is returned. With `||` the RHS is returned if the LHS is *any* falsey value. – Drew Reese Oct 19 '21 at 05:59
    • I don't understand this answer, could someone explain the syntax in more details ? – Hugo Feb 07 '22 at 14:37
    • @Hugo Is there a specific part you need help with? Which syntax would you like more detail on? Only thing "odd" or new might be the Nullish Coalescing Operator called out above, the rest is pretty standard React Javascript code. – Drew Reese Feb 07 '22 at 22:45
    • 1
      This was such a helpful answer, as I didn't know about using `createRef` instead of `useRef`. Note that for some use cases, `useRef([])` isn't necessary at all, particularly if the array of refs is being `map`ped from an array coming from state or props. – kevlarr May 16 '23 at 01:28
    9

    Mine is React Hooks version.

    useMemo to create an array of refs for performance sake.

    const vars = ['a', 'b'];
    const childRefs = React.useMemo(
        () => vars.map(()=> React.createRef()), 
        [vars.join(',')]
    );
    
    

    React will mount each ref to childRefs

    {vars.map((v, i) => {
        return (
            <div>
                 <Child v={v} ref={childRefs[i]} />
                 <button onClick={() => showAlert(i)}> click {i}</button>
            </div>
        )
     })
    }
    

    Here is a workable demo, hope that helps. ^_^

    
    
    const Child = React.forwardRef((props, ref) => {
    
      React.useImperativeHandle(ref, () => ({
        showAlert() {
          window.alert("Alert from Child: " + props.v);
        }
      }));
    
      return <h1>Hi, {props.v}</h1>;
    });
    
    const App = () => {
      const vars = ['a', 'b'];
      const childRefs = React.useMemo(
        () => vars.map(()=> React.createRef()), 
        // maybe vars.length
        [vars.join(',')]
      );
      function showAlert(index) {
        childRefs[index].current.showAlert();
      }
      
      return (
        <div>
          {
            vars.map((v, i) => {
              return (
                <div>
                  <Child v={v} ref={childRefs[i]} />
                  <button onClick={() => showAlert(i)}> click {i}</button>
                </div>
              )
            })
          }
        </div>
      )
    }
    
    
    const rootElement = document.getElementById("root");
    ReactDOM.render(
      <App />,
      rootElement
    );
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
    
    <div id="root"></div>
    Ace
    • 1,093
    • 2
    • 10
    • 23
    6

    There may be some Typescript inconsistencies and complexity in other answers. So I think the best way to use the useRef hook in a loop is:

    // Declaration
    const myRef = useRef([]);
    myRef.current = [];
    const addToRefs: (el) => void = (el) => {
      if (el && !myRef.current.includes(el)) {
        myRef.current.push(el);
      }
    };
    

    And

    // Assignment (input element example)
    ...
    ...
    {anyArrayForLoop.map((item, index) => {
       return (
           <input
               key={index}
               ref={addToRefs}
           />
       );
    })}
    ...
    ...
    

    Final:

    // The Data
    myRef.current
    
    sha'an
    • 1,032
    • 1
    • 11
    • 24
    1

    Instead of storing refs in an array, you could create a ref for each component within the loop. You can also access that ref in the parent component by a function.

    You could do something similar to this.

    const { useRef, useState } = React;
    
    const someArrayToMapStuff = ["a", "b", "c"];
    
    const ListWithRef = ({ text, setDisplayWhatsInsideRef }) => {
    
      const ref = React.useRef(null);
      
      const logAndDisplayInnerHTML = () => {
        setDisplayWhatsInsideRef(ref.current.innerHTML);
        console.log(ref.current);
      };
      
      return (
        <li 
          ref={ref}
          onClick={logAndDisplayInnerHTML}
        >
          <button>{text}</button>
        </li>
      );
      
    };
    
    const App = () => {
    
      const [displayWhatsInsideRef, setDisplayWhatsInsideRef] = useState("");
    
      return (
        <ul>
          {someArrayToMapStuff.map(thing => <ListWithRef 
            key={thing} 
            text={thing} 
            setDisplayWhatsInsideRef={setDisplayWhatsInsideRef}
          />)}
          
          {displayWhatsInsideRef && (
            <h1>Look, I'm a ref displaying innerHTML: {displayWhatsInsideRef}</h1>
          )}
        </ul>
      );
    };
    
    ReactDOM.createRoot(
        document.getElementById("root")
    ).render(<App />);
    <div id="root"></div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>

    Hopefully this helps someone.

    MK.
    • 752
    • 1
    • 8
    • 12