Refs have been around in React since the beginning, with string refs; but we now have a number of different ways to create and use refs.

Refs in React give us a way to get a reference to an actual underlying DOM node in a component or the component's instance.  With the introduction of hooks (which I love) and useRef, it can get confusing sometimes about which type of ref to use and how to go about it.

So, partly as a reminder for myself, as well as anyone else who might be struggling with React references, here's a short overview of some of the different types of refs I've used in my development, as well as how I might use them in context.

Callback Refs

Callback refs are functions passed to the ref attribute of an element or React component.  The function will be passed the instance of the rendered DOM element or the Component, the ref attribute is attached to.

class Input extends React.Component {
  constructor(props) {
    super(props);
    this.ref = null;
    this.setRef = el => this.ref = el;
  }
  
  componentDidMount() {
    // Focus the input when we mount in the DOM
    this.ref.focus();
  }
  
  render() {
    const { value } = this.props;
    return (
    	<input 
          type="text" 
          ref={this.setRef}
          value={value}
          />
    );
  }
}

React will call the ref function with the <input>'s DOM Element as an argument when the component mounts and with null when the component unmounts.  Callback refs like this are guaranteed to be up-to-date before the componentDidMount and componentDidUpdate lifecycle methods are called on the component.

Had we defined the callback ref function inline in our <input> element, like so:

<input ref={el => this.ref = el} /* ... */ />

That function would have been called twice, once with null and then again with the element.  This is because a new instance of the function is created on each render, so React needs to clear any previous ref and setup the new one.

You can use this method inside both class or functional components.

React.createRef

The primary method that refs are created since React 16.3 is the React.createRef method.  This method returns an object that has a .current property which will be assigned the DOM Element or Component instance.

class Input extends React.Component {
  constructor(props) {
    super(props);
    this.ref = React.createRef();
  }
  
  componentDidMount() {
    // Focus the input when we mount in the DOM
    this.ref.current.focus();
  }
  
  render() {
    const { value } = this.props;
    return (
    	<input 
          type="text" 
          ref={ref}
          value={value}
          />
    );
  }
}

With function components, you can still use createRef internally; but they don't expose the same lifecycle methods as class components.  So we could achieve the same effect as above using the useEffect hook in a functional component.

const Input = ({ value }) => {
  const ref = React.createRef();
  
  useEffect(() => ref.current.focus(), [])
  
  return (
    <input type="text" ref={ref} value={value}/>
  );
}

In this case, calling useEffect() with an empty dependency array, lets us execute that function only once after the component mounts to focus the input element.

useRef & useCallback for Refs

With the advent of hooks, we have two more primary mechanisms to create refs in our components.  

The first is the useRef hook.  useRef works similarly to React.createRef in that it gives us an object with a .current property.  It also guarantees that mutating the .current property won't cause our functional component to re-render.

const Input = ({ value }) => {
  const ref = React.useRef();
  
  useEffect(() => ref.current.focus(), [])
  
  return (
    <input type="text" ref={ref} value={value}/>
  );
}

Clearly very similar to using React.createRef, in that useRef returns an object whose .current property will be set to the DOM Element when the component mounts.

You can also create a callback ref using a combination of useState and useCallback as well.  Using this approach lets us perform an action when the element is mounted in the DOM, as we get notified when the callback function is called.

const Input = ({ value }) => {
  const [node, setNode] = useState();
  const ref = useCallback(el => setNode(el), []);
  
  // We can now watch `node` directly and perform
  // an action when it's been set.
  useEffect(() => {
    if (node) { node.focus(); }
  }, [node]);
  
  return (
    <input type="text" ref={ref} value={value} />
  );
}

We also could have left off the useEffect and performed our actions directly in the function passed to useCallback.  

Hopefully, this helped clear up some of the differences you come across when creating and using refs in your React code.  It definitely helped solidify some of these concepts in my own mind (even senior developers google topics like this on a daily basis)!

You can find some more details on refs in React at the following links:

  1. React Docs - Refs and the DOM
  2. React Docs - useRef Hook
  3. React Docs - callback refs with hooks (measuring a DOM node)