I am using useRef
in React to store an array of DOM element references (containerRefs). When I try to access an individual ref like containerRefs.current[index]
in the Dropdown component, I get undefined, even though containerRefs.current
, when logged with console.log()
inside useEffect, shows the expected array of references.
Inside useEffect, I am also setting setInitialised(true)
to re-render the component after mounting so that I can get the ref array.
import Dropdown from "./Dropdown";
import constants from "./constants";
export default function Home() {
const containerRefs = React.useRef([]);
const [intialised, setInitialised] = React.useState(false);
React.useEffect(() => {
console.log("Refs initialized:", containerRefs.current);
setInitialised(true)
}, []);
return (
<>
{constants.home_services.map((service, index) => (
<div
className={`w-full relative`}
key={index}
ref={(el) => containerRefs۔current[index]=el}
>
<div>
<h2 >
{service.name}
</h2>
</div>
/* All values passed to watchElements prop showa undefined on initial render */
{intialised && containerRefs.current[index] & (
<Dropdown
watchElements={containerRefs.current[index]}
>
<p className={`mt-2 p-3`}>
Lorem ipsum dolor sit amet consectetur,
</p>
</Dropdown>
)}
</div>
))}
</>
);
}
2
Answers
You can’t know when React will decide to set the value of the ref.
By this, you are telling React to execute the ref callback, but you can’t tell when React will actually execute this.
Your usage
watchElements={containerRefs.current[index]}
might execute before the execution of theref callback
What exactly happens is:
ref={(el) => containerRefs۔current[index]=el}
, not yet executedwatchElements={containerRefs.current[index]}
, since the ref callback is not yet executed,containerRefs.current
is still an empty array, socontainerRefs.current[index]
isundefined
and that leads towatchElements=undefined
undefined
is already passed aswatchElements
watchElements
leaves asundefined
There are few solutions you can apply.
If there are no additional renderings, don’t directly pass
containerRefs.current[index]
, but only the reference of thecontainerRefs
and a way to access its elements or pass a getter functionand inside Dropdown you can get the element by
watchElements()
.But it still won’t give you the value in the first render. You can only access it after the first render
Doing setInitialised(true) and checking the value before rendering Dropdown should do the trick
The above explains why the ref value seems undefined in first render, but with your code watchElements should not be undefined in Dropdown, since you are not rendering it if the value is falsy. I don’t see a problem, except few typos
Your question may demand answers for the following two other questions:
What is a render ?
What is a commit ?
As we know, in React render is just the invocation of the function hosting a component. And at the end of its call, the latest JSX with respect to the latest state will be returned.
Please note that nothing has been drawn on the screen till now. It means the DOM manipulation has not been occurred. This is an important point to keep in mind.
Commit is the next distinct phase wherein the screen will be updated. It includes DOM manipulation.
We discussed this much to get the basis to start our discussion about Ref.
Ref means reference to something which we know.
A Ref to a DOM element means the same – a reference to a DOM element.
Now another question comes in.
At which phase, React can set the reference ? Surely not during render, since render does have nothing to do with DOM update. The answer to the question is the commit phase. During this phase, the DOM is updated, and the reference or the ref variable will get the reference, till that time ref will be undefined since it has not been set by React.
Now coming to your question.
Let us inspect the below code snippet.
What has been going on here ? The code runs the rendering phase. And now we can easily agree that containerRefs.current[index] should be undefined during the entire time of the initial render. This is the reason you would get the same undefined value in the initial render.
Now the second point, you get valid Refs in useEffect. This is absolutely correct. useEffect runs after the commit phase. Now you may have got the reason for the second point as well.
Solution:
You have already got the solution in place. The following code of yours will do the job. It mandates that will render only when intialised is true which you set in the event useEffect. Please note that if intialised is true, then containerRefs.current[index] will also be true. Therefore the same can safely remove from the conditional.
Notes:
Between Render and the Commit, React performs its intelligence. It compares the two JSX – the latest JSX as well as the JSX just prior to the latest render. Here it finds the Delta JSX – the difference between the two JSX. Only the Delta JSX is passed to the commit phase. This way React optimises DOM updates and keeps a React app performant.
Based on your need, please go through the following documentation which is the basis of this post.
When React attaches the refs