How target DOM with react useRef in map
JavascriptReactjsReact HooksJavascript Problem Overview
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?
Javascript Solutions
Solution 1 - Javascript
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:
-
create array and keep it between renders by
useRef
-
we initialize each array's element by
createRef()
-
we can refer to list by using
.current
notationconst 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: ref
s are bound after element is rendered so during rendering is running for the first time it is not initialized yet.
Solution 2 - Javascript
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();
Solution 3 - Javascript
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.
Solution 4 - Javascript
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 useRef
s 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>
}
Solution 5 - Javascript
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>
)
}
Solution 6 - Javascript
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>
);
}