Issue
React.createRef
is really only valid in class-based components. If used in a functional component body then the ref would be recreated each render cycle.- Don’t use a DOM query selector to attach
onClick
listeners to DOM elements. These live outside react and you’d need to remember to clean them up (i.e. remove them) so you don’t have a memory leak. Use React’sonClick
prop. - When the
selectedElements
are mapped you attach the same ref to each element, so the last one set is the one your UI gets.
Solution
- Use
React.useRef
in the functional component body to store an array of react refs to attach to each element you want to scroll into view. - Attach the
scrollSmoothHandler
directly to eachspan
‘sonClick
prop. - Attach each ref from the ref array created in 1. to each mapped field set you want to scroll to.
Code
import React, { createRef, useRef } from "react";
import { render } from "react-dom";
const App = () => {
const selectedElements = [
"Item One",
"Item Two",
"Item Three",
"Item Four",
"Item Five"
];
// React ref to store array of refs
const scrollRefs = useRef([]);
// Populate scrollable refs, only create them once
// if the selectedElements array length is expected to change there is a workaround
scrollRefs.current = [...Array(selectedElements.length).keys()].map(
(_, i) => scrollRefs.current[i] ?? createRef()
);
// Curried handler to take index and return click handler
const scrollSmoothHandler = (index) => () => {
scrollRefs.current[index].current.scrollIntoView({ behavior: "smooth" });
};
return (
<div>
<div id="container">
<div className="_2iA8p44d0WZ">
{selectedElements.map((el, i) => (
<span
className="chip _7ahQImy"
onClick={scrollSmoothHandler(i)} // <-- pass index to curried handler
>
{el}
</span>
))}
<input
type="text"
className="searchBox"
id="search_input"
placeholder="Select"
autoComplete="off"
value=""
/>
</div>
</div>
<div>
{selectedElements.map((item, i) => (
<div
key={i}
className="selected-element"
ref={scrollRefs.current[i]} // <-- pass scroll ref @ index i
>
<fieldset>
<legend>{item}</legend>
</fieldset>
</div>
))}
</div>
</div>
);
};
Solution #2
Since you can’t update any elements in the div
with id="container"
and all the onClick
handlers need to be attached via querying the DOM, you can still use a curried scrollSmoothHandler
callback and enclose an index in scope. You’ll need an useEffect
hook to query the DOM after the initial render so the spans have been mounted, and an useState
hook to store a “loaded” state. The state is necessary to trigger a rerender and re-enclose over the scrollRefs
in the scrollSmoothHandler
callback.
const App = () => {
const selectedElements = [
"Item One",
"Item Two",
"Item Three",
"Item Four",
"Item Five"
];
const [loaded, setLoaded] = useState(false);
const scrollRefs = useRef([]);
const scrollSmoothHandler = (index) => () => {
scrollRefs.current[index].current.scrollIntoView({ behavior: "smooth" });
};
useEffect(() => {
const chipsArray = document.querySelectorAll("#container > div > .chip");
if (!loaded) {
scrollRefs.current = [...Array(chipsArray.length).keys()].map(
(_, i) => scrollRefs.current[i] ?? createRef()
);
chipsArray.forEach((elem, index) => {
elem.addEventListener("click", scrollSmoothHandler(index));
});
setLoaded(true);
}
}, [loaded]);
return (
<div>
<div id="container">
<div className="_2iA8p44d0WZ">
<span className="chip _7ahQImy">Item One</span>
<span className="chip _7ahQImy">Item Two</span>
<span className="chip _7ahQImy">Item Three</span>
<span className="chip _7ahQImy">Item Four</span>
<span className="chip _7ahQImy">Item Five</span>
<input
type="text"
className="searchBox"
id="search_input"
placeholder="Select"
autoComplete="off"
value=""
/>
</div>
</div>
<div>
{selectedElements.map((item, i) => (
<div key={i} className="selected-element" ref={scrollRefs.current[i]}>
<fieldset>
<legend>{item}</legend>
</fieldset>
</div>
))}
</div>
</div>
);
};