10.4 Refs and forwardRef — useRef<T> and Generic forwardRef
Two Uses of useRef
useRef serves two purposes:
- DOM element references: Initialize with
null, specify HTML element type in generic - Mutable value storage: Persist values without triggering re-renders
DOM Element References
import { useRef, useEffect } from 'react';
function AutoFocusInput() {
// DOM reference: initial value null, type is HTMLInputElement
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus(); // ?. used (could be null)
}, []);
return <input ref={inputRef} placeholder="Auto focus" />;
}
Common HTML Element Types
const divRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const selectRef = useRef<HTMLSelectElement>(null);
const formRef = useRef<HTMLFormElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const videoRef = useRef<HTMLVideoElement>(null);
const ulRef = useRef<HTMLUListElement>(null);
const tableRef = useRef<HTMLTableElement>(null);
DOM Manipulation Example
function ScrollableList() {
const listRef = useRef<HTMLUListElement>(null);
const scrollToTop = () => {
listRef.current?.scrollTo({ top: 0, behavior: 'smooth' });
};
const getHeight = (): number => {
return listRef.current?.offsetHeight ?? 0;
};
return (
<>
<ul ref={listRef} style={{ height: '300px', overflow: 'auto' }}>
{Array.from({ length: 100 }, (_, i) => (
<li key={i}>Item {i + 1}</li>
))}
</ul>
<button onClick={scrollToTop}>Back to Top</button>
</>
);
}
Mutable Value Storage
import { useRef, useState, useEffect } from 'react';
// Remember previous value
function usePrevious<T>(value: T): T | undefined {
const prevRef = useRef<T>();
useEffect(() => {
prevRef.current = value;
}, [value]);
return prevRef.current;
}
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<div>
<p>Current: {count}, Previous: {prevCount ?? 'none'}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</div>
);
}
// Store timer ID
function Debounce() {
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const [value, setValue] = useState('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
console.log('Search:', e.target.value);
}, 300);
};
useEffect(() => {
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
}, []);
return <input value={value} onChange={handleChange} />;
}
RefCallback (Callback Ref)
The type to use when passing a function as a ref.
import { RefCallback } from 'react';
function MeasureElement() {
const [height, setHeight] = useState(0);
// RefCallback<HTMLDivElement>
const measuredRef: RefCallback<HTMLDivElement> = (node) => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
};
return (
<div>
<div ref={measuredRef}>Content to measure</div>
<p>Height: {height}px</p>
</div>
);
}
forwardRef — Passing Ref to Child Components
Used when a parent component needs to access a child's DOM element.
Basic forwardRef
import { forwardRef, useImperativeHandle } from 'react';
interface InputProps {
label: string;
placeholder?: string;
}
// forwardRef<RefType, PropsType>
const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, placeholder }, ref) => {
return (
<div>
<label>{label}</label>
<input ref={ref} placeholder={placeholder} />
</div>
);
}
);
Input.displayName = 'Input'; // Name shown in DevTools
// Usage
function Form() {
const inputRef = useRef<HTMLInputElement>(null);
const focusInput = () => {
inputRef.current?.focus();
};
return (
<div>
<Input ref={inputRef} label="Name" placeholder="Enter name" />
<button onClick={focusInput}>Focus</button>
</div>
);
}
useImperativeHandle — Controlling Exposed Methods
Instead of exposing the entire DOM, provide only specific methods to the parent.
import { forwardRef, useImperativeHandle, useRef } from 'react';
// Define the type of methods to expose
interface VideoPlayerHandle {
play: () => void;
pause: () => void;
seek: (seconds: number) => void;
getCurrentTime: () => number;
}
interface VideoPlayerProps {
src: string;
autoPlay?: boolean;
}
const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
({ src, autoPlay = false }, ref) => {
const videoRef = useRef<HTMLVideoElement>(null);
// Define methods to expose to parent
useImperativeHandle(ref, () => ({
play: () => videoRef.current?.play(),
pause: () => videoRef.current?.pause(),
seek: (seconds: number) => {
if (videoRef.current) {
videoRef.current.currentTime = seconds;
}
},
getCurrentTime: () => videoRef.current?.currentTime ?? 0,
}));
return <video ref={videoRef} src={src} autoPlay={autoPlay} />;
}
);
// Usage
function VideoPage() {
const playerRef = useRef<VideoPlayerHandle>(null);
return (
<div>
<VideoPlayer ref={playerRef} src="/video.mp4" />
<button onClick={() => playerRef.current?.play()}>Play</button>
<button onClick={() => playerRef.current?.pause()}>Pause</button>
<button onClick={() => playerRef.current?.seek(30)}>Jump to 30s</button>
</div>
);
}
Generic forwardRef (Advanced)
TypeScript doesn't properly support generics with forwardRef. Here's a pattern to work around it.
// Problem: generics don't work properly with forwardRef
const List = forwardRef(<T,>(
{ items }: { items: T[] },
ref: React.ForwardedRef<HTMLUListElement>
) => (
<ul ref={ref}>
{items.map((item, i) => <li key={i}>{String(item)}</li>)}
</ul>
));
// Solution 1: Type assertion
const TypedForwardRef = forwardRef as <T>(
render: (props: { items: T[] }, ref: React.ForwardedRef<HTMLUListElement>) => React.ReactElement | null
) => (props: { items: T[] } & React.RefAttributes<HTMLUListElement>) => React.ReactElement | null;
// Solution 2: Function overloading (more practical)
function createForwardRef<T>(
render: (props: ListProps<T>, ref: React.ForwardedRef<HTMLUListElement>) => React.ReactElement | null
) {
return forwardRef(render) as unknown as <U extends T>(
props: ListProps<U> & React.RefAttributes<HTMLUListElement>
) => React.ReactElement | null;
}
Pro Tips
useRef type distinction: null vs undefined initial value
// null initial value: for DOM references (RefObject<T>)
const domRef = useRef<HTMLDivElement>(null);
// domRef.current: HTMLDivElement | null
// undefined initial value: for mutable values (MutableRefObject<T>)
const valueRef = useRef<number>();
// valueRef.current: number | undefined
// Value initialization: for mutable values
const timerRef = useRef<number>(0);
// timerRef.current: number (no null)
Initializing with null creates a RefObject (used like read-only), while initializing with undefined or a value creates a MutableRefObject (writable).