Published on

React Hooks: useRef & forwardRef

3032 words16 min read
Authors
  • avatar
    Name
    Curtis Warcup
    Twitter

Beyond the Basic React Hooks

I think it's safe to say that react developers have used the basic hooks like useState, useEffect, and useRef. But as you start to use react more and more, you will find that there are many more hooks. Here are some of the more advanced hooks that I have found useful.

useRef

Use the useRef hook when you want to "remember" a value, but don't want that value to trigger a re-render. This is useful when you want to store a value that you don't want to change, but you don't want to store it in state.

Examples include:

  • A reference to a DOM node
    • i.e. const inputRef = useRef() and then inputRef.current.focus()
  • A reference to a component instance
    • i.e. const componentRef = useRef() and then componentRef.current.someMethod()

Using useRef to store a value

  • import useRef from react

    t

  • inside your component, call useRef and pass in the initial value

import { useRef } from 'react'

export default function App() {
  const ref = useRef(0)

  console.log(ref.current) // returns 0

  return <div className="App">{ref.current}</div>
}
  • You can access the current value of the ref by calling ref.current
  • This current value is intentionally mutable. You can change it by assigning a new value to it.
    • You can read and write to it, but it will not trigger a re-render.
import { useRef } from 'react'

export default function App() {
  const ref = useRef(0)

  console.log(ref.current) // returns 0

  ref.current = 1

  console.log(ref.current) // returns 1

  ref.current++ // returns 2

  return <div className="App">{ref.current}</div>
}

Unlike state, ref is a plain JavaScript object contianing the current property. You can add any properties you want to it.

import { useRef } from 'react'

export default function App() {
  const ref = useRef(0)

  ref.current = 1

  ref.current++ // returns 2

  ref.current = 'hello' // returns 'hello'

  ref.current = { name: 'John' }

  return <div className="App">{ref.current.name}</div>
}

When a piece of information is used for rendering, keep it in state. When a piece of information is only needed by event handlers and changing it doesn’t require a re-render, using a ref may be more efficient.

Ref vs State

refstate
useRef(initialVal) returns {current: initialVal}useState(initialVal) returns [currentVal, setVal]
Does not trigger a re-renderTriggers a re-render when you change it
Mutable, meaning you can modify and update current value outside of the rendering processImmutable, meaning you can only update it with setVal. When this occurs, a re-render will occur.
Should not be used to read or write the current value during renderingYou can read state at any time. However, each render has its own snapshot of state which does not change.

useRef is could be though of like a specific flavor of useState. Take the following example:

// Inside of React
function useRef(initialValue) {
  const [ref, unused] = useState({ current: initialValue })
  return ref
}

During the first render, useRef return {current: initialValue}. This is the same as useState({current: initialValue}). However, useState will trigger a re-render when you change the value of current. useRef will not trigger a re-render.

Information obtained from beta.reactjs.

Examples of useRef

import { useState, useRef } from 'react'

export default function App() {
  const [text, setText] = useState('')
  const [isSending, setIsSending] = useState(false)
  const timeoutRef = useRef(null)

  function handleSend() {
    setIsSending(true)
    timeoutRef.current = setTimeout(() => {
      alert('Sent!')
      setIsSending(false)
    }, 3000)
  }

  function handleUndo() {
    setIsSending(false)
    clearTimeout(timeoutRef.current)
  }

  return (
    <>
      <input disabled={isSending} value={text} onChange={(e) => setText(e.target.value)} />
      <button disabled={isSending} onClick={handleSend}>
        {isSending ? 'Sending...' : 'Send'}
      </button>
      {isSending && <button onClick={handleUndo}>Undo</button>}
    </>
  )
}
import { useState, useRef } from 'react'

function DebouncedButton({ onClick, children }) {
  const timeoutRef = useRef(null)
  return (
    <button
      onClick={() => {
        clearTimeout(timeoutRef.current)
        timeoutRef.current = setTimeout(() => {
          onClick()
        }, 1000)
      }}
    >
      {children}
    </button>
  )
}

useRef to store a DOM node

Sometime you may need to access a DOM node directly. For example, you may want to focus on an input element when the page loads, or you may want to measure the size of an element. You can use the useRef hook to store a reference to a DOM node.

Getting a reference to a DOM node

import { useRef } from 'react'

export default function Form() {
  // Create a ref
  const inputRef = useRef(null)

  // Focus on the input element when the page loads
  function handleClick() {
    inputRef.current.focus()
  }

  return (
    <>
      {/* Get a reference to the input element */}
      <input ref={inputRef} />

      {/* When the button is clicked, focus on the input element */}
      <button onClick={handleClick}>Focus the input</button>
    </>
  )
}

Managing a list of refs using a ref callback

Sometimes you might need a ref to each item in the list, and you don’t know how many you will have. Something like this wouldn’t work:

<ul>
  {items.map((item) => {
    // Doesn't work!
    const ref = useRef(null)
    return <li ref={ref} />
  })}
</ul>

You cannot call useRef in a loop condition, or inside a map call.

The best solution to this is to pass a function to the ref attribute. This is known as a ref callback. The function will be called with the DOM node as an argument. You can store the node in a ref object.

This allows you to maintain your own array of refs, and you can access them later. You can also create a new Map object to store the refs.

import { useRef } from 'react'

export default function CatFriends() {
  // create a ref object
  const itemsRef = useRef(null)

  function scrollToId(itemId) {
    const map = getMap()
    const node = map.get(itemId)
    node.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'center',
    })
  }

  function getMap() {
    if (!itemsRef.current) {
      // Initialize the Map on first usage.
      itemsRef.current = new Map()
    }
    return itemsRef.current
  }

  return (
    <>
      <nav>
        <button onClick={() => scrollToId(0)}>Tom</button>
        <button onClick={() => scrollToId(5)}>Maru</button>
        <button onClick={() => scrollToId(9)}>Jellylorum</button>
      </nav>
      <div>
        <ul>
          {catList.map((cat) => (
            <li
              key={cat.id}
              ref={(node) => {
                const map = getMap()
                if (node) {
                  map.set(cat.id, node)
                } else {
                  map.delete(cat.id)
                }
              }}
            >
              <img src={cat.imageUrl} alt={'Cat #' + cat.id} />
            </li>
          ))}
        </ul>
      </div>
    </>
  )
}

const catList = []
for (let i = 0; i < 10; i++) {
  catList.push({
    id: i,
    imageUrl: 'https://placekitten.com/250/200?image=' + i,
  })
}

forwardRef

If you try to put a ref on your own component, you will get undefined as the value of the ref. This is because the ref is passed to the component as a prop, and the component doesn’t forward it to the DOM node.

For example, this code will not work:

import { useRef } from 'react'

// your own component
function MyInput(props) {
  return <input {...props} />
}

export default function MyForm() {
  // create a ref object in another component
  const inputRef = useRef(null)

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

  return (
    <>
      {/* Pass the ref to your own component */}
      <input ref={inputRef} />
      <button onClick={handleClick}>Focus the input</button>
    </>
  )
}

The ref prop is not forwarded to the DOM node. When you click the button, the input will not be focused.

This happens because by default React does not let a component access the DOM nodes of other components. Not even for its own children!

Solution: use forwardRef to pass the ref to the DOM node.

Instead, components that want to expose their DOM nodes have to opt in to that behavior. A component can specify that it “forwards” its ref to one of its children. Here’s how MyInput can use the forwardRef API:

import { forwardRef } from 'react'

const MyInput = forwardRef((props, ref) => {
  return <input {...props} ref={ref} />
})

Now, when you pass a ref to <MyInput>, the ref will be forwarded to the <input> element inside it.

import { useRef, forwardRef } from 'react'

// your own component
const MyInput = forwardRef((props, ref) => {
  return <input {...props} ref={ref} />
})

export default function MyForm() {
  // create a ref object in another component
  const inputRef = useRef(null)

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

  return (
    <>
      {/* Pass the ref to your own component */}
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>Focus the input</button>
    </>
  )
}

It is common to use forwardRef in components like buttons, inputs, and other components that need to be accessed by the parent component.

Best Practices for Refs

  • Only use ref's when you need to access the DOM node directly. Don't use it to store data.
    • examples of when you need to access the DOM node directly:
      • focus on an input element
      • measure the size of an element
      • add event listeners to an element
      • scroll position
      • calling browser APIs that React doesn't support
  • Don't use ref's to store data. Use useState instead.
  • Avoid changing DOM nodes managed by React.
    • Example: if you use ref.current.remove() to remove a DOM node, React will not be able to update it. If you called setState after that, React will not be able to update the DOM node.