popeating
popeating

Reputation: 454

useRef for element in loop in react

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?

    Upvotes: 15

    Views: 24452

  • Answers (4)

    Sha&#39;an
    Sha&#39;an

    Reputation: 1276

    This is how I use useRef in a loop to get a list of elements:

    • Declaration:

      const myRefs = useRef<HTMLInputElement[]>([]);
      
      const addToRefs = (el: HTMLInputElement) => {
        if (el && !myRefs.current.includes(el)) {
          myRefs.current.push(el);
        }
      };
      
    • Assignment:

      ...
      ...
      
      {anyArrayForLooping.map((item, index) => {
        return (
          <input
            key={index}
            ref={addToRefs}
          />
        );
      })}
      
      ...
      ...
      
    • Result:

      // Elements array:
      myRefs.current
      

    Upvotes: 9

    MK.
    MK.

    Reputation: 832

    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.

    Upvotes: 1

    Ace
    Ace

    Reputation: 1383

    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>

    Upvotes: 9

    Drew Reese
    Drew Reese

    Reputation: 202721

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

    Upvotes: 27

    Related Questions