[React Basics] Mastering the useRef Hook in React with Practical Examples

In the world of React, managing state and component lifecycle are core concepts. We're all familiar with useState for state management and useEffect for handling side effects. But there's another hook, quieter but incredibly powerful: useRef.

Mastering the useRef Hook in React with Practical Examples

If you've ever wondered how to directly access a DOM element, or needed to store a value without triggering a re-render every time it changes, then useRef is exactly what you're looking for.

What is useRef? An Overview

Basically, useRef is a React hook that lets you create a mutable "ref" object. This object has a single property: .current.

The syntax is simple:

import React, { useRef } from 'react'

const myRef = useRef(initialValue)

The most important thing about useRef is: Changing the value of myRef.current does NOT trigger a component re-render. This is the key difference from useState.

Think of useRef as a little box where you can store anything. You can change what's inside the box (.current) at any time, but because the box is "opaque" to React's rendering system, the component doesn't "care" about those changes.

Common use cases for useRef

There are two main scenarios where useRef shines—solving problems that useState can't or shouldn't handle.

1. Directly Accessing DOM Elements

This is the most common and powerful use of useRef. In React, we're encouraged to think "declaratively" rather than "imperatively." However, sometimes you need to interact directly with the DOM, such as:

  • Automatically focusing an input when a component mounts.
  • Managing animations or transitions.
  • Integrating with third-party libraries that require a direct DOM reference (e.g., a chart library).
  • Measuring the size or position of an element.

How it works:

  1. Create a ref: const myInputRef = useRef(null);
  2. Attach the ref to a JSX element via the ref attribute: <input ref={myInputRef} type="text" />
  3. Now, myInputRef.current points directly to the DOM node of the <input>. You can access all its properties and methods.

Classic example: Auto-focusing an input

import React, { useRef, useEffect } from 'react'

function AutoFocusInput() {
  // 1. Create the ref
  const inputRef = useRef(null)

  useEffect(() => {
    // 3. After the component mounts, call focus() on the input
    if (inputRef.current) {
      inputRef.current.focus()
    }
  }, []) // Empty array ensures this runs only once after mount

  return (
    <div>
      <label>Your name:</label>
      {/* 2. Attach the ref to the input */}
      <input ref={inputRef} type="text" placeholder="Enter your name..." />
    </div>
  )
}

2. Storing a Mutable Value Without Causing Re-render

Sometimes you need a value to persist across renders, you need to update it, but you don't want the component to re-render every time it changes. useState won't help here, but useRef is perfect.

The value in ref.current is preserved across renders.

Common use cases:

  • Storing timer IDs: like setTimeout or setInterval.
  • Storing the previous value of a state or prop.
  • Counting how many times a component has rendered.

Example: Counting renders

import React, { useState, useEffect, useRef } from 'react'

function RenderCounter() {
  const [count, setCount] = useState(0)

  // Create a ref with initial value 0
  const renderCount = useRef(0)

  useEffect(() => {
    // Increment the ref value after every render.
    // This does NOT cause an infinite render loop!
    renderCount.current = renderCount.current + 1
  }) // No dependency array, so this runs after every render

  return (
    <div>
      <p>Current state (count): {count}</p>
      <button onClick={() => setCount((c) => c + 1)}>Increment Count</button>
      <p>This component has rendered: {renderCount.current} times.</p>
    </div>
  )
}

In this example, every time you click "Increment Count", the count state changes and the component re-renders. useEffect runs, and renderCount.current is updated. Because updating the ref doesn't cause a re-render, you avoid an infinite loop, which would happen if you used useState to store the render count.

useRef vs. useState: When Should You Use Each?

This is a core question for becoming a skilled React developer. Remember this simple rule:

FeatureuseStateuseRef
Causes Re-render?YESNO
Update timingAsynchronousSynchronous
Main purposeManage state that affects the UIAccess DOM or store data not directly affecting the UI
When to use?When you want changes to be reflected on the screenWhen you need a "persistent box" for data across renders without triggering re-renders

Memory tip: Ask yourself, "Does the UI need to change when this value changes?" If YES, use useState. If NO, useRef is likely the better choice.

"Forwarding" Refs with forwardRef

Sometimes, you want a parent component to access a DOM element deep inside a child component. By default, you can't pass a ref directly to a custom component. This is where React.forwardRef comes in.

forwardRef is a higher-order component that lets your component "receive" a ref from its parent and "forward" it to a specific DOM element inside.

Example:

import React, { useRef, forwardRef, useImperativeHandle } from 'react'

// Child component wrapped in forwardRef
const FancyInput = forwardRef((props, ref) => {
  const inputRef = useRef()

  // (Advanced) Only expose the `focus` method to the parent
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus()
    },
    shake: () => {
      // Add shake animation here
      console.log('Shaking the input!')
    },
  }))

  return <input ref={inputRef} placeholder={props.placeholder} />
})

// Parent component
function App() {
  const fancyInputRef = useRef()

  const handleClick = () => {
    // Now you can call the exposed methods
    fancyInputRef.current.focus()
    fancyInputRef.current.shake()
  }

  return (
    <>
      <FancyInput
        ref={fancyInputRef}
        placeholder="Click the button to focus..."
      />
      <button onClick={handleClick}>Focus the Input</button>
    </>
  )
}

In this example, the App component can directly control FancyInput via the forwarded ref. The useImperativeHandle hook lets you customize and limit what the parent can access, improving encapsulation.

Conclusion: useRef is More Than Just a Tool

useRef isn’t just a way to "escape" React’s model and interact with the DOM. It’s a powerful and flexible hook that gives you a way to handle mutable values without being tied to the render lifecycle.

Understanding when and why to use useRef instead of useState will level up your React thinking, help you write more efficient and optimized code, and solve complex problems elegantly.

Next time you need to store data "silently," remember your trusty companion: useRef!

Related Posts

[React Basics] State in React: Concepts, Usage, and Detailed Examples

Learn the concepts, declaration, and usage of State to manage dynamic data in your React application. See detailed guide with examples.

[React Basics] Props in React: Concepts, Usage and Real-world Examples

Decode props in ReactJS: from basic concepts to effectively passing data between components. Explore easy-to-understand examples that you can apply immediately.

[React Basics] Guide to Setting Up a React Development Environment

A step-by-step guide for beginners to set up a React development environment. Learn how to install Node.js, npm, and create your first React project with Create React App or Vite.

[React Basics] Rendering Lists in React: Best Practices and Performance Optimization

Learn techniques for rendering lists in React, from basic .map() usage to advanced methods for handling dynamic data and optimizing performance. This guide is for all levels.