useRef is one of React's built-in hooks that provides a way to access and interact with DOM elements directly or to persist values across renders without causing re-renders. Think of it as a "box" that can hold a mutable value in its .current property that persists for the full lifetime of the component.
Access DOM elements directly to call imperative methods (like focus() or play())
Store values that need to persist between renders without triggering re-renders
Keep track of previous state or prop values
Store mutable values that don't affect the UI
Regular variables are reset on each render, and state variables trigger re-renders when changed, but useRef provides a way to store persistent values that don't cause re-renders when updated.
import { useRef } from"react";functionTextInput() {// Create a ref with an initial value of nullconstinputRef=useRef(null);// Function to focus the input elementconstfocusInput= () => {// Access the DOM element through the .current propertyinputRef.current.focus(); };return ( <div> {/* Attach the ref to a DOM element */} <inputref={inputRef} type="text" /> <buttononClick={focusInput}>Focus the input</button> </div> );}
Let's break this down:
We import the useRef hook from React
We call useRef(null) to create a ref object, with null as the initial value
The ref object has a mutable .current property, which initially points to null
We attach the ref to the input element using the ref attribute
After rendering, inputRef.current will point to the actual DOM element
We can then call DOM methods on this element, like focus()
Refs can store values that persist across renders without triggering re-renders:
functionRenderCounter() {// This state update will cause a re-renderconst [count,setCount] =useState(0);// This ref tracks render count but doesn't cause re-renders when updatedconstrenderCount=useRef(0);// Update the ref on each renderuseEffect(() => {renderCount.current +=1; });return ( <div> <p>You clicked {count} times</p> <p>This component has rendered {renderCount.current} times</p> <buttononClick={() =>setCount(count +1)}>Click me</button> </div> );}
Storing Previous Values
Refs can store previous values of props or state for comparison:
functionCounterWithPrevious() {const [count,setCount] =useState(0);constprevCountRef=useRef();useEffect(() => {// Store current count value in ref after renderprevCountRef.current = count; }, [count]);// Get previous count value (undefined on first render)constprevCount=prevCountRef.current;return ( <div> <p> Now: {count}, Before: {prevCount !==undefined? prevCount :"N/A"} </p> <buttononClick={() =>setCount(count +1)}>Increment</button> </div> );}
Creating Instance Variables
Refs can be used to create instance-like variables in function components:
functionStopwatch() {const [time,setTime] =useState(0);const [isRunning,setIsRunning] =useState(false);// Use ref for values that shouldn't trigger re-rendersconstintervalRef=useRef(null);conststartTimer= () => {if (!isRunning) {setIsRunning(true);intervalRef.current =setInterval(() => {setTime((t) => t +1); },1000); } };conststopTimer= () => {if (isRunning) {clearInterval(intervalRef.current);intervalRef.current =null;setIsRunning(false); } };// Clean up the interval on unmountuseEffect(() => {return () => {if (intervalRef.current) {clearInterval(intervalRef.current); } }; }, []);return ( <div> <p>Time: {time} seconds</p> <buttononClick={startTimer} disabled={isRunning}> Start </button> <buttononClick={stopTimer} disabled={!isRunning}> Stop </button> </div> );}
In TypeScript, you should specify the type of the ref's current value:
// For DOM elementsconstinputRef=useRef<HTMLInputElement>(null);// For custom valuesconstcountRef=useRef<number>(0);
When working with DOM refs, there are two patterns to be aware of:
Read-only ref: If you only need to read from the DOM element and don't need to initialize it with a value:
// The null assertion (!) is not needed when accessing currentconstinputRef=useRef<HTMLInputElement>(null);// Checking if ref is assigned before using itconstfocusInput= () => {if (inputRef.current) {inputRef.current.focus(); }};
Mutable ref: If you need to both initialize the ref and modify its current value:
// The type has to allow for nullconstcountRef=useRef<number|null>(null);// Initialize in useEffect or event handlersuseEffect(() => {countRef.current =0;}, []);
It's important to understand when to use useRef versus useState:
// Use useState when:// 1. The value should be displayed in the UI// 2. Changes to the value should trigger re-rendersconst [count,setCount] =useState(0);// Use useRef when:// 1. The value shouldn't trigger re-renders when changed// 2. You need to access DOM elements// 3. You need to persist values between rendersconstcountRef=useRef(0);
Key differences:
Updating state with setCount causes a re-render; updating countRef.current does not
count is immutable within a render; countRef.current can be changed anytime
State updates are queued and batched; ref updates happen immediately
constcanvasRef=useRef(null);useEffect(() => {if (canvasRef.current) {constcontext=canvasRef.current.getContext("2d");// Use the canvas context for drawing }}, []);return <canvasref={canvasRef} width={500} height={300} />;
Callback Refs for Dynamic Refs
Sometimes you need more control over when and how a ref is attached:
functionMeasureExample() {const [height,setHeight] =useState(0);// Callback ref that measures the elementconstmeasuredRef=useCallback((node) => {if (node !==null) {setHeight(node.getBoundingClientRect().height); } }, []);return ( <> <h1ref={measuredRef}>Hello, world</h1> <p>The above header is {Math.round(height)}px tall</p> </> );}
Avoiding Common Mistakes
Don't use refs for values that should trigger re-renders:
// Wrong: UI won't update when count changesconstcountRef=useRef(0);constincrement= () => {countRef.current +=1;};// Right: UI updates when count changesconst [count,setCount] =useState(0);constincrement= () => {setCount((c) => c +1);};
Don't read or write refs during rendering:
// Wrong: Reading refs during rendering can cause inconsistenciesfunctionComponent() {constref=useRef(0);// Don't do this during renderingref.current +=1;return <div>{ref.current}</div>;}// Right: Update refs in event handlers or effectsfunctionComponent() {constref=useRef(0);const [,forceUpdate] =useState({});consthandleClick= () => {ref.current +=1;forceUpdate({}); };return ( <div> <p>Count: {ref.current}</p> <buttononClick={handleClick}>Increment</button> </div> );}
Objective: Create a component with a button that, when clicked, displays an input field and automatically focuses on it.
Detailed Instructions:
Create a new file called auto-focus-input.tsx in your components folder.
Import React, useState, useRef, and useEffect.
Create a functional component named AutoFocusInput.
Inside your component:
Create a state variable isVisible to control the input's visibility
Create a ref for the input element
Add a button that toggles the input's visibility
When the input becomes visible, it should automatically get focus
Use the useEffect hook to focus the input when it becomes visible.
Return JSX that includes the button and conditionally renders the input based on the isVisible state.
Starter Code:
import { useState, useRef, useEffect } from"react";constAutoFocusInput= () => {// Add your state for visibility// Add your ref for the input element// Add your effect for auto-focusing// Add your toggle functionreturn <div>{/* Add your button and conditional rendering for the input */}</div>;};exportdefault AutoFocusInput;
Exercise 2: Previous Value Tracker
Objective: Create a component that displays both the current and previous values of a counter.
Detailed Instructions:
Create a new file called previous-value-tracker.tsx in your components folder.
Import React, useState, useRef, and useEffect.
Create a functional component named PreviousValueTracker.
Inside your component:
Create a state variable count for the counter
Create a ref previousCountRef to store the previous count value
Use an effect to update the ref after each render
Add buttons to increment and decrement the counter
Display both the current and previous values in your component.
The previous value should be "None" on the first render.
Starter Code:
import { useState, useRef, useEffect } from"react";constPreviousValueTracker= () => {// Add your state for the counter// Add your ref for the previous value// Add your effect to update the previous valuereturn ( <div> {/* Display the current and previous values */} {/* Add buttons to update the counter */} </div> );};exportdefault PreviousValueTracker;
Exercise 3: Click Outside Detector
Objective: Create a dropdown component that closes when the user clicks outside of it, using refs to detect outside clicks.
Detailed Instructions:
Create a new file called dropdown-with-click-outside.tsx in your components folder.
Import React, useState, useRef, and useEffect.
Create a functional component named DropdownWithClickOutside.
Inside your component:
Create a state variable isOpen to track if the dropdown is open
Create a ref for the dropdown container element
Add an effect that adds a click event listener to the document
The event listener should check if the click occurred outside the dropdown
If the click was outside, close the dropdown
Remember to clean up the event listener when the component unmounts.
Include a button to toggle the dropdown and a dropdown menu with some items.
Starter Code:
import { useState, useRef, useEffect } from"react";constDropdownWithClickOutside= () => {// Add your state for the dropdown open/closed state// Add your ref for the dropdown container// Add your effect for the click outside detectionreturn <div>{/* Add the dropdown toggle button and menu */}</div>;};exportdefault DropdownWithClickOutside;
Use refs to access DOM elements when you need to call imperative methods
Use refs to store values that shouldn't trigger re-renders when they change
Don't read or update refs during rendering, do it in effects or event handlers
Refs are mutable and persist across renders
Clean up any event listeners or subscriptions created with refs
With TypeScript, always specify the correct type for your refs
Now that you understand useRef, you can work with DOM elements directly and efficiently maintain values throughout a component's lifecycle without unnecessary re-renders!