Skip to main content
Advertisement

10.4 Refs and forwardRef — useRef<T> and Generic forwardRef

Two Uses of useRef

useRef serves two purposes:

  1. DOM element references: Initialize with null, specify HTML element type in generic
  2. 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).

Advertisement