Using refs in React components

React keeps a snapshot of the UI in memory (virtual DOM) and syncs it with the real DOM when necessary. It re-renders components when props are modified or state is updated. However, in certain cases, components can be modified without triggering the re-render. This is where React refs come in handy.

Refs

Refs provide direct access to DOM nodes or React elements which allows you to modify a component without re-rendering it. This technique is useful when, for instance, you need to manage input focus state or handle text selection. Another use case is integrating with third-party DOM libraries (more on this here).

class App extends Component {
  constructor(props) {
    super(props);

    // [1] create a ref
    this.username = React.createRef();
  }

  handleFormSubmission(e) {
    e.preventDefault();
    
    // [3] get the actual the input element node by accessing .current property
    const node = this.username.current;
    
    if (node && !node.value) {
      // [4] set focus on the node if it doesn't have a value
      // we can do whatever we want here e.g. apply styling,
      // add event handlers, etc.
      node.focus();
    }    
  }

  render() {
    // [2] attach the ref to the input element
    return (
      <form onSubmit={(e) => this.handleFormSubmission(e)}> 
        <input ref={this.username}/> 
        <button type="submit">Login</button> 
      </form>
    )
  }
}

Something to note here is that refs cannot be used on functional components. A functional component is stateless and does not have any lifecycle methods. It does not have an instance, therefore, a ref cannot be attached to it. If you want to attach a ref to a functional component, convert it into a class first.

// [1] create a functional component
const UsernameInput = () => {
  return (
    <input name="username"/>
  )
}

class App extends Component {
  constructor(props) {
    super(props);

    // [2] create a ref
    this.username = React.createRef();
  }

  render() {
    // [3] attaching the ref to the functional component won't work as functional components do not have instances
    return (
      <form> 
        <UsernameInput ref={this.username} /> 
        <button type="submit">Login</button> 
      </form>
    )
  }
}

Using refs inside of functional components is possible as long as a DOM element or class component is referenced.

// [1] create a functional component with a ref inside
const UsernameInput = () => {
  const input = React.createRef();

  // [3] set the value of the input element
  const handleClick = function() {
    input.current.value = 'Clicked';
  }

  // [2] attach the ref to the input element
  return (
    <input name="username" ref={input} onClick={handleClick} />
  );
}

Ref forwarding

In some situations, we might want to create a ref outside of the component and then pass the ref into it. By doing so we can forward a ref to child components hence the name of this technique – ref forwarding. This is quite useful when we have reusable components and we want to manage things (like focus state, text selection outside, etc.) of the components themselves.

// [1] create a functional component and obtain the ref passed into it
const InputComponent = React.forwardRef((props, ref) => {
  return <input ref={ref} />;
});

class Form extends Component {
  constructor(props) {
    super(props);

    // [2] create a ref outside of the reusable child component
    this.inputRef = React.createRef();

    this.handleFormSubmission = this.handleFormSubmission.bind(this);
  }

  handleFormSubmission(e) {
    e.preventDefault();

    const username = e.target.username.value;

    if (typeof username !== 'string' || !username.length) {
      console.log('Username invalid');
    }
  }

  componentDidMount() {
    // [4] set focus on the input element inside the InputComponent when the Form component is mounted
    this.formBtnRef.current.focus();
  }

  render() {
    // [3] pass ref into the child component
    return (
      <form onSubmit={this.handleFormSubmission}>
        <InputComponent ref={this.inputRef} />
        <button type="submit">Add</button>
      </form>
    );
  }
}

We can apply the same technique to HOCs (higher-order components).

// [1] create a functional component and obtain the ref passed into it
const InputComponent = React.forwardRef((props, ref) => {
  return <input ref={ref} />;
});

const passRef = (WrappedComponent) => {
  class NewComponent extends Component {
    constructor(props) {
      super(props);

      this.state = { data: [] };      
    }

    render() {
      // [6] pass ref to the input component
      return (
        <form onSubmit={this.handleFormSubmission}>
          <WrappedComponent ref={this.props.forwardedRef} />
          <button type="submit">Add</button>
        </form>
      );
    }
  }

  // [5] forward ref as a new forwardedRef prop to the NewComponent
  return React.forwardRef((props, ref) => {
    return <NewComponent forwardedRef={ref} {...props} />;
  });
};

class HocComponent extends Component {
  constructor(props) {
    super(props);

    // [2] create a ref
    this.inputRef = React.createRef();
  }

  // [7] set focus on the input element (which is inside of the HOC component) on component mount
  componentDidMount() {
    this.inputRef.current.focus();
  }

  render() {
    return (
      // [3] create a HOC
      const hocComponent = passRef(InputComponent);

      // [4] pass ref into the HOC component
      return <hocComponent ref={this.inputRef} />;
    );
  }
}

Refs and ref forwarding is a great way to access DOM nodes and interact with child components without re-rendering them. Apply these techniques wisely and do not overuse them. Have fun!