Search code examples
reactjsreact-ref

How does reassignment to DOM of React ref work in React?


I'm trying to understand how the React ref works when using it to manipulate DOM. I'm borrowing a modified version of the first example on the React's website (v18.3.1) -> Learn as an example:

In the following two code, the first throws an error, but the second works fine. What is happening under-the-hood?

1.

import { useRef } from 'react';

export default function Form() {
  const inputRef = useRef(null);
  inputRef.current = 2;    // the only difference

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <input ref={inputRef} />
      <button onClick={handleClick}>
        Focus the input
      </button>
    </>
  );
}

gives Runtime Error App.js: inputRef.current.focus is not a function (8:21)

[---Edit1---]

I did the test by directly modifying/copying the code in React's website tutorial's interactive code region. However, as shown by (@Drew Reese)'s answer below, this error is not neccessarily replicable. I'm guessing this is an unintential usage of React and the mechanisms under the hood might be inconsistent across different versions.

To me it seems like the reasonable behavior is to not give an error.

[---End of Edit1---]

2.

import { useRef } from 'react';

export default function Form() {
  const inputRef = useRef(null);
  inputRef.current = null;   // the only difference

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <input ref={inputRef} />
      <button onClick={handleClick}>
        Focus the input
      </button>
    </>
  );
}

According to React

React sets ref.current during the commit. Before updating the DOM, React sets the affected ref.current values to null. After updating the DOM, React immediately sets them to the corresponding DOM nodes.

I'm interpreting that during every React update, the inputRef.current will get reassigned.

Then shouldn't anything I've done to inputRef.current be ignored and overwritten by <input ref={inputRef} />? Why does it throw an error if I had set inputRef.current to 2?

Another example of reassigning the inputRef:

import { useRef, useState } from 'react';

export default function Form() {
  const inputRef = useRef(null);
  const inputRef2 = useRef(null);
  const [state, setState] = useState(1);

  function handleClick() {
    inputRef.current.focus();
    inputRef.current = inputRef2.current;
    setState(state+1);                      // just to trigger re-render
  }

  return (
    <>
      {state}
      <input ref={inputRef} />              // does this reset inputRef everytime it re-renders?
      <input ref={inputRef2} />
      <button onClick={handleClick}>
        Focus the input
      </button>
    </>
  );
}

In this case, the first click will focus on the first input box, and the second click will focus on the second input box. I don't understand how did that happen. What exactly does the line <input ref={inputRef} /> do during every re-render?


Solution

  • Part 1

    For the first part I'm not able to reproduce the issue/error you describe, both implementations work.

    1. Setting the current ref value to 2

          function Form() {
            const inputRef = React.useRef(null);
            inputRef.current = 2;    // the only difference
      
            function handleClick() {
              inputRef.current.focus();
            }
      
            return (
              <React.Fragment>
                <input ref={inputRef} />
                <button onClick={handleClick}>
                  Focus the input
                </button>
              </React.Fragment>
            );
          }
      
          const rootElement = document.getElementById("root");
          const root = ReactDOM.createRoot(rootElement);
      
          root.render(
            <React.StrictMode>
              <Form />
            </React.StrictMode>
          );
          <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
          <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
          <div id="root" />

    2. Setting the current ref value to null

          function Form() {
            const inputRef = React.useRef(null);
            inputRef.current = null;   // the only difference
      
            function handleClick() {
              inputRef.current.focus();
            }
      
            return (
              <React.Fragment>
                <input ref={inputRef} />
                <button onClick={handleClick}>
                  Focus the input
                </button>
              </React.Fragment>
            );
          }
      
          const rootElement = document.getElementById("root");
          const root = ReactDOM.createRoot(rootElement);
      
          root.render(
            <React.StrictMode>
              <Form />
            </React.StrictMode>
          );
          <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
              <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
              <div id="root" />

    I'm interpreting that during every React update, the inputRef.current will get reassigned according to React

    React sets ref.current during the commit. Before updating the DOM, React sets the affected ref.current values to null. After updating the DOM, React immediately sets them to the corresponding DOM nodes.

    Then shouldn't anything I've done to inputRef.current be ignored and overwritten by <input ref={inputRef} />? Why does it throw an error if I had set inputRef.current to 2?

    The part you are quoting from is the When React attaches the refs, as in when they are attached to a DOMNodes when the component mounts. Both the inputRef.current = 2; and inputRef.current = null; are directly in the function component body and are both unintentional side-effects. The refs are mutated during the "Render Phase", but then correspondingly set to null and then to the DOMNode after the DOM has been updated during the "Commit Phase".

    Note the next part of that section:

    Usually, you will access refs from event handlers. If you want to do something with a ref, but there is no particular event to do it in, you might need an Effect.

    Consider the following:

    function Form() {
      const inputRef = React.useRef(null);
      
      React.useEffect(() => {
        inputRef.current = 2;    // the only difference
      });
    
      function handleClick() {
        inputRef.current.focus();
      }
    
      return (
        <React.Fragment>
          <input ref={inputRef} />
          <button onClick={handleClick}>
            Focus the input
          </button>
        </React.Fragment>
      );
    }
    
    const rootElement = document.getElementById("root");
    const root = ReactDOM.createRoot(rootElement);
    
    root.render(
      <React.StrictMode>
        <Form />
      </React.StrictMode>
    );
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
    <div id="root" />

    function Form() {
      const inputRef = React.useRef(null);
      
      React.useEffect(() => {
        inputRef.current = null;   // the only difference
      });
    
      function handleClick() {
        inputRef.current.focus();
      }
    
      return (
        <React.Fragment>
          <input ref={inputRef} />
          <button onClick={handleClick}>
            Focus the input
          </button>
        </React.Fragment>
      );
    }
    
    const rootElement = document.getElementById("root");
    const root = ReactDOM.createRoot(rootElement);
    
    root.render(
      <React.StrictMode>
        <Form />
      </React.StrictMode>
    );
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
    <div id="root" />

    Note now that after the ref mutation is converted to an intentional side-effect at the end of the render cycle that both implementations now error, TypeError: inputRef.current.focus is not a function and TypeError: Cannot read properties of null (reading 'focus') respectively.

    Part 2

    In this case, the first click will focus on the first input box, and the second click will focus on the second input box. I don't understand how did that happen.

    The component mounted and attached and set the React refs to the input DOMNodes as explained above. The code/callback handler then mutated the ref values later.

    What exactly does the line <input ref={inputRef} /> do during every re-render?

    Nothing really.

    Consider the following example:

    function Form() {
      const inputRef = React.useRef(null);
      const inputRef2 = React.useRef(null);
      const [state, setState] = React.useState(1);
      
      function handleClick() {
        console.log("Inside 1", {
          inputRef: inputRef.current.name,
          inputRef2: inputRef2.current.name,
        });
    
        inputRef.current.focus();
        inputRef.current = inputRef2.current;
        setState(state + 1); // just to trigger re-render
    
        console.log("Inside 2", {
          inputRef: inputRef.current.name,
          inputRef2: inputRef2.current.name,
        });
      }
    
      console.log("Outside", {
        inputRef: inputRef.current && inputRef.current.name,
        inputRef2: inputRef2.current && inputRef2.current.name,
      });
    
      return (
        <React.Fragment>
          <input name="1" ref={inputRef} />
          <input name="2" ref={inputRef2} />
          <button onClick={handleClick}>
            Focus the input
          </button>
        </React.Fragment>
      );
    }
    
    const rootElement = document.getElementById("root");
    const root = ReactDOM.createRoot(rootElement);
    
    root.render(
      <React.StrictMode>
        <Form />
      </React.StrictMode>
    );
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
    <div id="root" />

    It appears the refs are attached during the initial render cycle, but then you later mutate them to point to something else, or to hold different current values. React refs are basically "buckets" that can be mutated at any time and their values persist from render cycle to render cycle. It doesn't appear that React "resets" the refs back to the DOMNode they were initially attached to. In other words, React doesn't re-attach refs to DOMNodes on subsequent render cycles.

    The above console logs explained:

    // Initial render cycle
    Outside {
      "inputRef": null,
      "inputRef2": null
    }
    
    // Press the button
    Inside 1 {
      "inputRef": "1", // <-- ref1 points to input 1
      "inputRef2": "2" // <-- ref2 points to input 2
    }
    Inside 2 {
      "inputRef": "2", // <-- ref1 points to input 2 now
      "inputRef2": "2" // <-- ref1 points to input 2
    }
    
    // Component rerender
    Outside {
      "inputRef": "2", // <-- ref1 still points to input 2
      "inputRef2": "2" // <-- ref1 points to input 2
    }
    
    // Press the button
    Inside 1 {
      "inputRef": "2", // <-- ref1 still points to input 2
      "inputRef2": "2" // <-- ref1 points to input 1
    }
    Inside 2 {
      "inputRef": "2", // <-- ref1 still points to input 2
      "inputRef2": "2" // <-- ref1 points to input 2
    }
    
    // Component rerender
    Outside {
      "inputRef": "2", // <-- ref1 still points to input 2
      "inputRef2": "2" // <-- ref1 points to input 2
    }
    

    The basic lesson here is to not mutate your React refs that are used to access the DOM so they can always reference the underlying DOMNode element.