Technology · React
React useRef Hook
Use useRef to access DOM elements directly and store mutable values.
TL;DR
- 01Use useRef to get direct access to DOM nodes.
- 02Store mutable values that don't trigger re-renders.
- 03useImperativeHandle exposes methods to parent components.
Accessing DOM Elements
- Create a ref to access DOM nodes 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 call methods.
function VideoPlayer() { const videoRef = useRef(null); return ( <> <video ref={videoRef} /> <button onClick={() => videoRef.current.play()}> Play </button> </> ); } - Refs persist across re-renders without triggering them.
- Read scroll position or dimensions from a DOM element via ref.
function ScrollTracker() { const boxRef = useRef(null); function logSize() { const { width, height } = boxRef.current.getBoundingClientRect(); console.log(width, height); } return <div ref={boxRef} onClick={logSize}>Click me</div>; } - Attach refs to any host element: input, div, canvas, video, and more.
const canvasRef = useRef(null); useEffect(() => { const ctx = canvasRef.current.getContext("2d"); ctx.fillRect(0, 0, 100, 100); }, []); return <canvas ref={canvasRef} width={200} height={200} />;
Storing Mutable Values
- Use refs to store values that don't cause re-renders.
function Counter() { const countRef = useRef(0); function increment() { countRef.current++; console.log(`Count: ${countRef.current}`); } return ( <button onClick={increment}> Increment (see console) </button> ); } - Track previous state or props with refs.
function Counter({ value }) { const prevValueRef = useRef(); useEffect(() => { prevValueRef.current = value; }, [value]); return <p>Now: {value}, Before: {prevValueRef.current}</p>; } - Refs don't trigger re-renders when updated.
- Store a ref to the latest callback to avoid stale closure issues in effects.
function useEvent(handler) { const handlerRef = useRef(handler); useEffect(() => { handlerRef.current = handler; }); return useCallback((...args) => handlerRef.current(...args), []); } - Use a ref to track whether the component is still mounted before updating state.
function useSafeState(initial) { const isMounted = useRef(true); const [state, setState] = useState(initial); useEffect(() => () => { isMounted.current = false; }, []); const safeSet = useCallback(v => { if (isMounted.current) setState(v); }, []); return [state, safeSet]; }
useImperativeHandle
- Expose custom methods to parent components.
const TextInput = forwardRef((props, ref) => { const inputRef = useRef(null); useImperativeHandle(ref, () => ({ focus: () => inputRef.current.focus(), clear: () => { inputRef.current.value = ""; } })); return <input ref={inputRef} />; }); function Parent() { const inputRef = useRef(null); return ( <> <TextInput ref={inputRef} /> <button onClick={() => inputRef.current.focus()}> Focus </button> </> ); } - Only expose methods you want parents to call.
- forwardRef is required to receive a ref prop in a function component.
const Input = forwardRef((props, ref) => <input ref={ref} {...props} />); function Form() { const ref = useRef(null); return <Input ref={ref} placeholder="Name" />; } - Restrict exposed API to prevent parents from misusing the component.
useImperativeHandle(ref, () => ({ focus: () => inputRef.current.focus(), // scrollIntoView NOT exposed — parent shouldn't control that })); - Pass a second argument to useImperativeHandle to control when it re-runs.
useImperativeHandle(ref, () => ({ getValue: () => value }), [value]);
Managing Timers and Intervals
- Store timer IDs in refs to clean up 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 intervals on unmount with useEffect.
useEffect(() => { return () => { if (intervalRef.current) { clearInterval(intervalRef.current); } }; }, []); - Store a debounce timeout ref to cancel the previous call on each keystroke.
const timeoutRef = useRef(null); function handleChange(e) { clearTimeout(timeoutRef.current); timeoutRef.current = setTimeout(() => search(e.target.value), 300); } - Use requestAnimationFrame refs for smooth animations without state updates.
const rafRef = useRef(null); function startAnimation() { function step() { /* update canvas */ rafRef.current = requestAnimationFrame(step); } rafRef.current = requestAnimationFrame(step); } useEffect(() => () => cancelAnimationFrame(rafRef.current), []);
Best Practices
- Use refs sparingly — prefer state and props first.
// Bad: using ref when state would work const countRef = useRef(0); countRef.current++; // Good: use state for rendering const [count, setCount] = useState(0); - Never read or write refs during rendering.
// Bad: reading ref during render function Component() { const ref = useRef(null); console.log(ref.current); // undefined during render return <div ref={ref}>Content</div>; } // Good: read in effect useEffect(() => { console.log(ref.current); // now it has a value }, []); - Initialize refs with initial values if needed.
const ref = useRef(initialValue); - Type refs correctly in TypeScript by specifying the element type.
const inputRef = useRef<HTMLInputElement>(null); inputRef.current?.focus(); // optional chain because it may be null - Avoid storing derived state in refs — compute it during render instead.
// Bad: keeping a ref in sync with state manually const doubleRef = useRef(count * 2); // Good: just derive during render const double = count * 2;
Tip: Use refs for DOM access and imperative operations, but prefer state and props for most state management needs.
Warning: Changing ref.current doesn't trigger a re-render — if you need UI updates, use state instead.