skip to Main Content

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


  1. You can’t know when React will decide to set the value of the ref.

    ref={(el) => containerRefs۔current[index]=el}
    

    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 the ref callback

    What exactly happens is:

    1. You pass ref={(el) => containerRefs۔current[index]=el}, not yet executed
    2. You pass watchElements={containerRefs.current[index]}, since the ref callback is not yet executed, containerRefs.current is still an empty array, so containerRefs.current[index] is undefined and that leads to watchElements=undefined
    3. React executes the ref callback and fills the empty array, but the undefined is already passed as watchElements
    4. Since no additional rendering occurs, watchElements leaves as undefined

    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 the containerRefs and a way to access its elements or pass a getter function

    watchElements={() => containerRefs.current[index]}
    

    and 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

    • if you pass it directly as watchElements, you will need an additional render in the parent
    • if you pass it as function, it will work inside an effect in Dropdown
    • if you pass it as function, it will require an additional render in Dropdown, only if you want to use it inside the body of the Dropdown

    Doing setInitialised(true) and checking the value before rendering Dropdown should do the trick

    intialised && containerRefs.current[index] && (
     <Dropdown watchElements={containerRefs.current[index]} />)
    

    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

    Login or Signup to reply.
  2. 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.

    <Dropdown watchElements={containerRefs.current[index]}>
    ...
    </Dropdown>
    

    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.

    {intialised && containerRefs.current[index] & 
    ( <Dropdown watchElements={containerRefs.current[index]} > ... </Dropdown> )}
    

    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

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search