How target DOM with react useRef in map
Asked Answered
D

8

61

I looking for a solution about get an array of DOM elements with react useRef() hook.

example:

const Component = () => 
{

  // In `items`, I would like to get an array of DOM element
  let items = useRef(null);

  return <ul>
    {['left', 'right'].map((el, i) =>
      <li key={i} ref={items} children={el} />
    )}
  </ul>
}

How can I achieve this?

Discriminating answered 1/3, 2019 at 8:8 Comment(0)
K
84

useRef is just partially similar to React's ref(just structure of object with only field of current).

useRef hook is aiming on storing some data between renders and changing that data does not trigger re-rendering(unlike useState does).

Also just gentle reminder: better avoid initialize hooks in loops or if. It's first rule of hooks.

Having this in mind we:

  1. create array and keep it between renders by useRef

  2. we initialize each array's element by createRef()

  3. we can refer to list by using .current notation

    const Component = () => {
    
      let refs = useRef([React.createRef(), React.createRef()]);
    
      useEffect(() => {
        refs.current[0].current.focus()
      }, []);
    
      return (<ul>
        {['left', 'right'].map((el, i) =>
          <li key={i}><input ref={refs.current[i]} value={el} /></li>
        )}
      </ul>)
    }
    

This way we can safely modify array(say by changing it's length). But don't forget that mutating data stored by useRef does not trigger re-render. So to make changing length to re-render we need to involve useState.

const Component = () => {

  const [length, setLength] = useState(2);
  const refs = useRef([React.createRef(), React.createRef()]);

  function updateLength({ target: { value }}) {
    setLength(value);
    refs.current = refs.current.splice(0, value);
    for(let i = 0; i< value; i++) {
      refs.current[i] = refs.current[i] || React.createRef();
    }
    refs.current = refs.current.map((item) => item || React.createRef());
  }

  useEffect(() => {
   refs.current[refs.current.length - 1].current.focus()
  }, [length]);

  return (<>
    <ul>
    {refs.current.map((el, i) =>
      <li key={i}><input ref={refs.current[i]} value={i} /></li>
    )}
  </ul>
  <input value={refs.current.length} type="number" onChange={updateLength} />
  </>)
}

Also don't try to access refs.current[0].current at first rendering - it will raise an error.

Say

      return (<ul>
        {['left', 'right'].map((el, i) =>
          <li key={i}>
            <input ref={refs.current[i]} value={el} />
            {refs.current[i].current.value}</li> // cannot read property `value` of undefined
        )}
      </ul>)

So you either guard it as

      return (<ul>
        {['left', 'right'].map((el, i) =>
          <li key={i}>
            <input ref={refs.current[i]} value={el} />
            {refs.current[i].current && refs.current[i].current.value}</li> // cannot read property `value` of undefined
        )}
      </ul>)

or access it in useEffect hook. Reason: refs are bound after element is rendered so during rendering is running for the first time it is not initialized yet.

Katiakatie answered 11/3, 2019 at 16:1 Comment(8)
You're a boss, thanks so much for laying this out! useRef(elts.map(React.createRef)) is exactly what I was looking for to keep the function component from throwing my refs away. – Marcelina
beautiful πŸ‘πŸ‘πŸ‘ – Gym
Hi, can you explain why you needed React.createRef() two times? – Decimalize
@Decimalize because OP asked for getting refs for exactly 2 elements – Katiakatie
Thank you very much for the explanation, @skyboyer. I have made it dynamic using like this- const refs = useRef( new Array(2).fill(React.createRef())); . Will this cause a problem? – Decimalize
Anyone know how to implement this in a typescript version? – Monoicous
@Decimalize the problem with that is it will create a single ref and then populate the useRef array with that.. So, if you try to reference any of those items, they will all reference the one entity. const refs = useRef([...new Array(3)].map(() => React.createRef())); is a better alternative I've found. – Statfarad
Upvote if you just miss using PHP ; ( – Polypus
E
16

I'll expand on skyboyer's answer a bit. For performance optimization (and to avoid potential weird bugs), you might prefer to use useMemo instead of useRef. Because useMemo accepts a callback as an argument instead of a value, React.createRef will only be initialized once, after the first render. Inside the callback you can return an array of createRef values and use the array appropriately.

Initialization:

  const refs= useMemo(
    () => Array.from({ length: 3 }).map(() => createRef()),
    []
  );

Empty array here (as a second argument) tells React to only initialize refs once. If ref count changes you may need to pass [x.length] as "a deps array" and create refs dynamically: Array.from({ length: x.length }).map(() => createRef()),

Usage:

  refs[i+1 % 3].current.focus();
Enamel answered 4/12, 2019 at 13:50 Comment(2)
could you give some details why useMemo is preffered and what bugs do you mean? – Katiakatie
I'm getting: "Warning: Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks" – Vowelize
A
13

take the parent Reference and manipulate the childrens

const Component = () => {
  const ulRef = useRef(null);

  useEffect(() => {
    ulRef.current.children[0].focus();
  }, []);

  return (
    <ul ref={ulRef}>
      {['left', 'right'].map((el, i) => (
        <li key={i}>
          <input value={el} />
        </li>
      ))}
    </ul>
  );
};

I work this way and I think that's more simple than other proposed answers.

Ashtray answered 1/1, 2021 at 14:29 Comment(2)
interesting idea – Alcatraz
You are a genius sir. Ty so much for this smart and simple answer. – Mathian
R
5

Instead of using array of refs or something like that, you can seperate each map item to component. When you seperate them, you can use useRefs independently:

const DATA = [
  { id: 0, name: "John" },
  { id: 1, name: "Doe" }
];

//using array of refs or something like that:
function Component() {
  const items = useRef(Array(DATA.length).fill(createRef()));
  return (
    <ul>
      {DATA.map((item, i) => (
        <li key={item.id} ref={items[i]}>
          {item.name}
        </li>
      ))}
    </ul>
  );
}


//seperate each map item to component:
function Component() {
  return (
    <ul>
      {DATA.map((item, i) => (
        <MapItemComponent key={item.id} data={item}/>
      ))}
    </ul>
  );
}

function MapItemComponent({data}){
  const itemRef = useRef();
  return <li ref={itemRef}>
    {data.name}
  </li>
}
Rehearing answered 22/1, 2021 at 9:12 Comment(2)
Added @StephenOstermiller – Rehearing
Thanks for the update, looks much better. – Gritty
A
2

If you know the length of the array ahead of time, to which you do in your example you can simply create an array of refs and then assign each one by their index:

const Component = () => {
  const items = Array.from({length: 2}, a => useRef(null));
  return (
    <ul>
      {['left', 'right'].map((el, i)) => (
        <li key={el} ref={items[i]}>{el}</li>
      )}
    </ul>
  )
}
Alodium answered 1/3, 2019 at 8:22 Comment(4)
React Hook "useRef" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function Any idea why? – Intellectual
@LirenYeo Due to the Hooks Rules, specifically that React relies on the order in which Hooks are called. A Hook called in a callback would make for an unstable sequence of calls. – Sepoy
so it should be const items = useRef(Array.from({length: 2}, () => React.createRef())) – Katiakatie
Does this actually work for anybody? – Ceuta
A
1

I had a problem like this and read 'Joer's answer and realised you can just loop through using the index setting the querySelector class dynamically and set only one ref to the overall parent. Apologies for the load of code but hope this helps someone:

import React, { useRef, useState } from 'react';
import { connectToDatabase } from "../util/mongodb";

export default function Top({ posts }) {
  //const [count, setCount] = useState(1);
  const wrapperRef = useRef(null);


  const copyToClipboard = (index, areaNumber) => {
    // 
    // HERE I AM USING A DYNAMIC CLASS FOR THE WRAPPER REF 
    // AND DYNAMIC QUERY SELECTOR, THEREBY ONLY NEEDING ONE REF ON THE TOP PARENT
    const onePost = wrapperRef.current.querySelector(`.index_${index}`)
    const oneLang = onePost.querySelectorAll('textarea')[areaNumber];
    oneLang.select();
    document.execCommand('copy');
  };

  var allPosts = posts.map((post, index) => {

    var formattedDate = post.date.replace(/T/, ' \xa0\xa0\xa0').split(".")[0]
    var englishHtml = post.en1 + post.en2 + post.en3 + post.en4 + post.en5;
    var frenchHtml = post.fr1 + post.fr2 + post.fr3 + post.fr4 + post.fr5;
    var germanHtml = post.de1 + post.de2 + post.de3 + post.de4 + post.de5;

    return (
      <div className={post.title} key={post._id}>
        <h2>{formattedDate}</h2>
        <h2>{index}</h2>

        <div className={"wrapper index_" + index}>
          <div className="one en">
            <h3>Eng</h3>
            <button onClick={() => {copyToClipboard(index, 0)}}>COPY</button>
            <textarea value={englishHtml} readOnly></textarea>
          </div>

          <div className="one fr">
            <h3>Fr</h3>
            <button onClick={() => {copyToClipboard(index, 1)}}>COPY</button> 
            <textarea value={frenchHtml} readOnly></textarea>
          </div>

          <div className="one de">
            <h3>De</h3>
            <button onClick={() => {copyToClipboard(index, 2)}}>COPY</button>
            <textarea value={germanHtml} readOnly></textarea>
          </div>
        </div>

      </div>
    )
  })

  return (
    <div ref={wrapperRef}>
      <h1>Latest delivery pages </h1>
      <ul>
        {allPosts}
      </ul>

      <style jsx global>{`

        body{
          margin: 0;
          padding: 0;
        }
        h1{
          padding-left: 40px;
          color: grey;
          font-family: system-ui;
          font-variant: all-small-caps;
        }
        .one,.one textarea {
          font-size: 5px;
          height: 200px;
          width: 300px;
          max-width:  350px;
          list-style-type:none;
          padding-inline-start: 0px;
          margin-right: 50px;
          margin-bottom: 150px;
          
        }

        h2{
          font-family: system-ui;
          font-variant: all-small-caps;
        }
        .one h3 {
          font-size: 25px;
          margin-top: 0;
          margin-bottom: 10px;
          font-family: system-ui;
        }

        .one button{
          width: 300px;
          height: 40px;
          margin-bottom: 10px;
        }

        @media screen and (min-width: 768px){
          .wrapper{
            display: flex;
            flex-direction: row;
          }
        }

      `}</style>
    </div>
  );
}
Alcatraz answered 26/1, 2021 at 20:23 Comment(0)
R
-1

In this case you would need to create an array of empty refs and push the refs while they are generated inside the React component.

You would then use useEffect to handle the corresponding refs.

Here's an example:

const refs = []

useEffect(() => {
    refs.map(ref => {
      console.log(ref.current)
      // DO SOMETHING WITH ref.current
    })
  }, [refs])

{postsData.map((post, i) => {
   refs.push(React.createRef())
   return (
     <div ref={refs[i]} key={post.id}>{...}</div>
   )}
}
Roslynrosmarin answered 7/6, 2022 at 13:49 Comment(0)
K
-1

Late I know, but I came across a solution using .childNodes that I thought was genius. I'm not 100% sure it works in your use-case but was perfect for me:

    const navList = [
        { title: 'Add an event', link: '/createEvent' },
        { title: 'Contact me', link: '/contact' },
        { title: 'About this site', link: '/about' },
    ];

    // useRef to access DOM elements
    const navBarRef = useRef<HTMLUListElement>(null);
    const navItemRef = useRef<HTMLLIElement>(null);

    // map navList to create navItems
    const navItemsMapped = (
        <ul className={styles.navBar} ref={navBarRef}>
            {navList.map((item, index) => {
                return (
                    // note ref assigned to each li element
                    <li key={index} className={styles.navItem} ref={navItemRef}>
                        <Link href={item.link}>{item.title}</Link>
                    </li>
                );
            })}
        </ul>
    );


    const navButtonClickHandler = () => {
        // navBar styles
        const navBar = navBarRef.current;
        navBar?.classList.toggle(styles.navBarActive);
        // navItem styles
        const navItems = navBar?.childNodes;
        // dynamically assign ref to each navItem based on their status as childNodes of navBar, rather than using a static ref
        navItems?.forEach((item) => {
            const itemRef = item as HTMLLIElement;
            itemRef.classList.toggle(styles.navItemActive);
        });
    };

They key here is the final forEach, where each ref is assigned based on the navItem status as child node of navBar, rather than statically. I should also note this is an excerpt from NextJS/TypeScript.

Kingston answered 26/4, 2023 at 13:46 Comment(0)

© 2022 - 2024 β€” McMap. All rights reserved.