Technology · React
React Ref Patterns
Use useRef for DOM access, timers, and storing mutable values.
TL;DR
- 01Use useRef to access DOM nodes directly when needed.
- 02Store mutable values that don't trigger re-renders.
- 03Use forwardRef to pass refs to child components.
DOM Access with useRef
- Create a ref to access DOM elements directly.
function TextInput() { const inputRef = useRef(null); function handleClick() { inputRef.current.focus(); } return ( <> <input ref={inputRef} /> <button onClick={handleClick}>Focus input</button> </> ); } - Access element properties and methods.
function VideoPlayer() { const videoRef = useRef(null); function handlePlay() { videoRef.current.play(); } function handlePause() { videoRef.current.pause(); } return ( <> <video ref={videoRef} /> <button onClick={handlePlay}>Play</button> <button onClick={handlePause}>Pause</button> </> ); }
Managing Timers and Intervals
- Store timer IDs in refs to clear them later.
function Stopwatch() { const intervalRef = useRef(null); const [time, setTime] = useState(0); function start() { intervalRef.current = setInterval(() => { setTime(t => t + 1); }, 1000); } function stop() { clearInterval(intervalRef.current); } return ( <> <p>Time: {time}s</p> <button onClick={start}>Start</button> <button onClick={stop}>Stop</button> </> ); } - Clean up timers on unmount.
useEffect(() => { return () => { if (intervalRef.current) { clearInterval(intervalRef.current); } }; }, []);
Storing Mutable Values
- Use refs to store values that don't trigger re-renders.
function Counter() { const countRef = useRef(0); function increment() { countRef.current++; console.log(`Count: ${countRef.current}`); } return ( <> <p>Ref count: {countRef.current}</p> <button onClick={increment}>Increment</button> </> ); } - Track previous state with refs.
function Counter() { const [count, setCount] = useState(0); const prevCountRef = useRef(); useEffect(() => { prevCountRef.current = count; }, [count]); return <p>Now: {count}, Before: {prevCountRef.current}</p>; } - Store flags and metadata without re-renders.
const isFirstRender = useRef(true); useEffect(() => { if (isFirstRender.current) { isFirstRender.current = false; // run only on mount } }, []);
forwardRef for Child Components
- Pass refs to child components with forwardRef.
const TextInput = forwardRef((props, ref) => ( <input ref={ref} /> )); function Parent() { const inputRef = useRef(null); return ( <> <TextInput ref={inputRef} /> <button onClick={() => inputRef.current.focus()}> Focus </button> </> ); } - Expose custom methods with useImperativeHandle.
const TextInput = forwardRef((props, ref) => { const inputRef = useRef(null); useImperativeHandle(ref, () => ({ focus: () => inputRef.current.focus(), clear: () => { inputRef.current.value = ""; } })); return <input ref={inputRef} />; });
Common Patterns
- Detect clicks outside a ref element.
function Dropdown() { const dropdownRef = useRef(null); const [open, setOpen] = useState(false); useEffect(() => { function handleClickOutside(e) { if (dropdownRef.current && !dropdownRef.current.contains(e.target)) { setOpen(false); } } document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, []); return <div ref={dropdownRef}>{/* dropdown content */}</div>; } - Measure element size with refs.
function SizeDisplay() { const divRef = useRef(null); const [size, setSize] = useState(null); function measureSize() { const rect = divRef.current.getBoundingClientRect(); setSize({ width: rect.width, height: rect.height }); } return ( <div ref={divRef} onClick={measureSize}> Size: {size && `${size.width}x${size.height}`} </div> ); }
Tip: Use refs sparingly — prefer state and props for most cases. Refs are for when you need direct DOM access or to store mutable values.
Warning: Changing ref.current doesn't trigger a re-render — if you need to update the UI, use state instead.