Reputation: 454
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
ref={(el) => (`line${i}` = el)}
which is not working any other solution i could try?
Upvotes: 15
Views: 24452
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
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
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
Reputation: 202721
This won't work as each render when menu
is mapped it creates new react refs.
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