Exploring useRef and forwardRef in React: DOM Access and Beyond
In React, controlling and accessing DOM elements directly can be tricky, especially within the confines of functional components. This is where the useRef and forwardRef hooks come in handy. useRef provides a way to persist values across renders without causing re-renders, while forwardRef enables parent components to interact with child components’ DOM elements directly. In this post, we’ll explore how these hooks work, their common use cases, and some potential pitfalls to avoid, allowing you to make the most of React’s ref system.
1. useRef Definition
The useRef hook allows you to create a mutable reference that persists across re-renders of a component. In essence, it is mainly used for accessing and interacting with DOM elements and storing values that persist across renders without causing re-renders.
Unlike state variables, updating a reference does not trigger a re-render of the component, which makes it ideal for storing information that doesn’t affect the visual output of the component.
Keep in mind that useRef is a widely misused hook with many pitfalls, all of which will be covered here.
Some general use cases for the useRef hook are as follows:
- Accessing dom elements
- Persisting values across renders
- Keeping track of previous state values
Note: In class components, you can use class variables to persist values between render, but in function components, you must use the useRef hook to persist values between renders.
const ref = useRef(initialValue)You can provide a default value in useRef invocation to initialize the ref with a value. The initial value parameter is ignored after the initial render.
useRef returns an object with a single property called “current”, which holds the current ref value.
2. Manipulating DOM
The most common use of refs is to access and manipulate DOM elements. For example:
import { useRef } from 'react'
function MyComponent() {
const inputRef = useRef(null)
// ...Then, pass the ref object as the “ref” attribute using JSX to the DOM element you wish to manipulate:
// ...
return <input ref={inputRef} />And just like that, you can now use the inputRef.current to access that DOM node and call methods like focus():
function handleClick() {
inputRef.current.==focus==()
}React conveniently sets the ref to null if the node gets removed from the DOM.
3. Takeaways
- useRef stores information that persists between re-renders (unlike regular variables, which reset on every render)
- Updating a ref does not trigger a re-render of the component (unlike state variables, which trigger a re-render)
- The value stored in a ref is local to each copy of the component (unlike the variables outside, which are shared)
4. Common Pitfalls Regarding Misuse (Or Non-use) of useRef
Infinite Re-renders
A common pitfall is accidentally triggering infinite re-renders of a component, often due to improper use of state or effects. This typically happens when a state variable is updated inside an effect that depends on that very state, creating a feedback loop that continuously triggers re-renders. For example:
import { useState, useEffect } from 'react'
function MyComponent() {
const [count, setCount] = useState(0)
useEffect(() => {
setCount(count + 1) // this will cause an infinite loop
})
return <div>{count}</div>
}Here is why this is an infinite loop:
- Every time the component renders, the effect gets run
- Every time the effect gets run, the state gets updated
- Every time the state gets updated, the component is re-rendered
And thus, it’s an infinite loop. When you try to use state variables within a useEffect to track a “previous value,” you can inadvertently create a situation where the component re-renders endlessly, especially if the effect updates the state.
One solution to this problem would be to use useRef instead, because useRef can store data that persists between renders without causing re-renders. Here is how that would look like:
import { useState, useEffect, useRef } from 'react'
function MyComponent() {
const [count, setCount] = useState(0)
const prevCountRef = useRef()
useEffect(() => {
prevCountRef.current = count
}, [count])
const prevCount = prevCountRef.current
return (
<div>
<p>Current Count: {count}</p>
<p>Previous Count: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
)
}Accessing Ref in During Rendering
Refs (ref.current) should not be read or written during rendering (inside the return statement of a function component, or the render function of a class component), as it would make the component’s behavior unpredictable.
React expects that components return the same output given the same input (props and state) without any side effects. In other words, the rendering phase must remain pure.
This is why React strict mode renders components twice: to help detect accidental impurities early on in development.
For example, this is a mistake:
import { useRef, useEffect } from 'react'
function MyComponent() {
const myRef = useRef(0)
useEffect(() => {
myRef.current += 1
console.log("ref value after effect:", myRef.current)
})
return <div>{myRef.current}</div> // !!DON'T DO THIS!!
}You should not read or write ref.current during rendering. You should read or write refs from event handlers or effects instead.
5. Optimized Initializations
In React, the initial value assigned to a ref is preserved after the first render and isn’t recalculated in subsequent renders.
function Video() {
const playerRef = useRef(new HugeClass())
// ...In this example, even though the new HugeClass() call is only relevant for the initial render, it will still get invoked on every render. This can cause performance issues if the object creation is resource-intensive.
To avoid this, you can initialize the ref more efficiently:
function Video() {
const playerRef = useRef(null)
if (playerRef.current === null) {
playerRef.current = new HugeClass()
}
// ...Typically, directly reading or writing to ref.current during a render is discouraged. However, in this scenario, it’s acceptable because the logic is straightforward and only runs during the initial setup, ensuring consistent results.
6. forwardRef
forwardRef lets you pass a ref from a parent component down into a child, so the parent can reach a DOM element that lives inside that child. You need it because, by default, components can’t be given refs in React.
For example, passing a ref to a component like this would NOT be possible by default:
const inputRef = useRef(null)
return <ChildComponent ref={inputRef} />
// if "ChildComponent" is not wrapped in a forwardRef,
// this code will NOT work!You would get an error in console saying that function components cannot be given refs.
To solve this problem, you can wrap the child component in forwardRef function, which will allow you to pass a ref from the parent component to the child component. For example, let’s say this was the child component:
export default function ChildComponent({ value, onChange }) {
return (
<input
value={value}
onChange={onChange}
/>
)
}Wrapping it in forwardRef would look like this:
import { forwardRef } from 'react'
export default const ChildComponent = forwardRef((ref) =>
<input ref={ref} />
)Alternatively, you may choose to write it like this (it’s personal preference, but this approach probably looks cleaner than above):
import { forwardRef } from 'react'
const ChildComponent = (ref) => {
<input ref={ref} />
}
export default forwardRef(ChildComponent)Now the ref flows down into the child, and the parent can use it to reach a DOM element rendered inside that child.