Go back

useRef instead of querySelector in React

Coming from vanillajs the ideal approach of accessing and manipulating elements on a webpage involves the use of document.querySelector(), and I've carried this ideology into my thought process in React. It is wrong. I shouldn't have been doing that, but, I did not know any better.

In React, it is recommended to use the useRef() hook rather than any of the document object's API — getElementById or querySelector — to perform DOM manipulation operations. Why? You might ask.

Well, this is because manipulating the DOM directly with these APIs/methods can lead to unexpected behaviors sometimes, and it can be less efficient than using the virtual DOM.

Unexpected behaviors

Some of the unexpected behaviors that you may experience when you try to manipulate DOM elements directly;

  • Race conditions: When multiple components are trying to manipulate the same DOM element, it can lead to race conditions and unpredictable behavior. For example, one component might remove an element from the DOM while another component is still trying to manipulate it.

  • Accessibility issues: Manipulating the DOM directly can also cause accessibility issues for users who rely on screen readers or other assistive technologies. If you manipulate the DOM in a way that changes the order or structure of the content, it can make it difficult or impossible for these users to access the information.

  • Inefficient updates: When you manipulate the DOM directly, the browser needs to recalculate the layout and repaint the affected elements, which can be a slow and expensive operation. This can lead to performance issues and slow down your application.

With useRef() you can create a reference to a DOM element and access its properties and methods without any of the complexities listed above. Guillermo Rauch wrote an article — 7 principles of rich web applications.

In this particular section, he talks about the necessity of updating the DOM and went ahead to mention Dan Abramov's Hot Reloading in React concept. Unrelated but this sorta fixes the Inefficient updates behavior you'd encounter when you manipulate the DOM directly.

useRef for form validation

Let's walk through the process of validating a form component in React with useRef. The approach I use occasionally involved manipulating the DOM directly. I'd find myself doing something like this;

import React from 'react'
 
export default function FormComponent() {
  const [name, setName] = React.useState('')
 
  const handleChange = (e) => {
    setName(e.target.value)
  }
 
  const handleSubmit = () => {
    const nameInput = document.querySelector('#name').value
 
    // checks if the input field is empty.
    // if it is an alert is fired.
    if (nameInput === '') {
      alert('name cannot be blank')
    }
  }
 
  return (
    <form onSubmit={handleSubmit}>
      <input
        id="name"
        name="full name"
        type="text"
        value={name}
        onChange={handleChange}
        placeholder="full name"
      />
    </form>
  )
}

And when you have multiple input fields, you'll start modifying the contents of handleSubmit() to accommodate the DOM elements.

With useRef, the function that validates the user input — handleSubmit() — and the state variable, name is modified like so. I've also added an error variable to watch the state of the input.

export default function FormComponent() {
  const inputRef = React.useRef(null)
  const [error, setError] = React.useState(false)
 
  const handleSubmit = (e) => {
    e.preventDefault()
    const value = inputRef.current.value
 
    if (!value) {
      setError(true)
    } else {
      // Submit the form
    }
  }
 
  const handleChange = () => {
    const value = inputRef.current.value
 
    if (value) {
      setError(false)
    }
  }
 
  return (
    <form onSubmit={handleSubmit}>
      <input type="text" ref={inputRef} onChange={handleInputChange} />
      <button type="submit">Submit</button>
      {error && <p>name field cannot be blank, pleaseee!</p>}
    </form>
  )
}

In the modified version of <FormComponent /> above, I created a reference to the input element using useRef() and used it to access its value in the handleSubmit() function.

I created a variable to store the error state and update it based on the input value in the handleChange() function. Then I proceed to render the error message if the error state is true.

useRef to animate DOM elements

Yes, it is similar to the way we can alter the style of an element in vanillajs, say when an action is fired. It could be the click of a button, or when an element scrolls into view.

const element = document.querySelector('#element')
 
element.style.display = 'none'

In the component below, the useRef hook is used to create a reference to the div element that we want to animate. There's a state variable isAnimating which keeps track of whether or not the animation is currently playing.

export const RefExample = () => {
  const boxRef = React.useRef(null)
  const [isAnimating, setIsAnimating] = React.useState(false)
 
  function handleStartAnimation() {
    setIsAnimating(true)
    boxRef.current.style.transform = 'translateX(300px)'
    setTimeout(() => {
      setIsAnimating(false)
      boxRef.current.style.transform = ''
    }, 1000)
  }
 
  return (
    <div className="App">
      <div className={`box ${isAnimating ? 'is-animating' : ''}`} ref={boxRef}>
        <p>Hello, I'm an animated box!</p>
      </div>
      <button onClick={handleStartAnimation}>Start Animation</button>
    </div>
  )
}

When the "Start Animation" button is clicked, the handleStartAnimation() function is called. This function sets isAnimating to true and applies a transform property to the boxRef.current element using the DOM API. This causes the element to move 300 pixels to the right.

setTimeout resets the element's transform property after 1 second. This allows the element to return to its original position and complete the animation.

You'll also notice how we're conditionally adding a CSS class — is-animating — to the box element based on the isAnimating state variable. This allows us to apply additional styling to the element during the animation, such as changing its color or opacity.

className={`box ${isAnimating ? 'is-animating' : ''}`}

Here's what the box and is-animating classes looks like;

.box {
  height: 120px;
  width: 120px;
  background-color: ghostwhite;
}
 
.is-animating {
  animation: inflate 0.4s ease-in-out;
  transition: all 0.4s ease-in-out;
}
 
@keyframes inflate {
  from {
    transform: scale(0.9);
  }
  to {
    transform: scale(1.07);
  }
}

When you observe the snippet above closely, you'll see that it uses a @keyframes rule to initialize the animation. When the isAnimating state is true, it increases the size of the box and moves it 300px away from its original position and back.

Where else can I go?

With refs in React, we can minimize the aforementioned unexpected behaviors since it allows us to create a reference to any DOM element, and access its properties and methods in a more controlled and efficient way.

Another example of what you can do with useRef() can be found here, in this guide