useRef for element in loop in react
Asked Answered
B

4

14

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?

  • Barbarian answered 18/12, 2020 at 0:10 Comment(3)
    You probably want React.useRef() not React.createRef(). createRef() is only for use in React class components.Mexicali
    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.Mexicali
    @Mexicali 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 would make that clear.Hetaerism
    H
    23

    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>
      );
    })}
    
    Hetaerism answered 18/12, 2020 at 0:53 Comment(5)
    complains about ?? is it && or || ?Moya
    @RaulH Technically, neither, but the 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.Hetaerism
    I don't understand this answer, could someone explain the syntax in more details ?Hower
    @Hower 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.Hetaerism
    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 mapped from an array coming from state or props.Ovariectomy
    A
    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>
    Adjourn answered 20/1, 2021 at 3:38 Comment(0)
    C
    9

    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
      
    Culosio answered 14/1, 2022 at 13:8 Comment(1)
    Simple and clear, you saved my life. Thank you.Afterimage
    A
    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.

    Aftmost answered 8/2, 2023 at 15:17 Comment(0)

    © 2022 - 2024 — McMap. All rights reserved.