Hack Frontend Community

Refs in React (useRef, createRef, forwardRef)

What are Refs?

Refs (references) are a way to get direct access to DOM elements or React components from code.

Refs are used when you need to:

  • Manage focus, text selection
  • Trigger animations
  • Integrate with third-party libraries
  • Measure element dimensions

useRef

useRef is a hook for creating refs in functional components.

Accessing DOM elements

import { useRef } from 'react';

function TextInput() {
  const inputRef = useRef(null);

  function handleClick() {
    // Get direct access to input
    inputRef.current.focus();
  }

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={handleClick}>Focus Input</button>
    </div>
  );
}

Storing mutable value

useRef can be used to store any value that doesn't trigger re-render:

function Timer() {
  const [count, setCount] = useState(0);
  const intervalRef = useRef(null);

  function start() {
    if (intervalRef.current) return;
    
    intervalRef.current = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
  }

  function stop() {
    if (intervalRef.current) {
      clearInterval(intervalRef.current);
      intervalRef.current = null;
    }
  }

  useEffect(() => {
    return () => stop(); // Cleanup
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </div>
  );
}

useRef vs useState

// useState - triggers re-render
const [value, setValue] = useState(0);

// useRef - does NOT trigger re-render
const valueRef = useRef(0);
valueRef.current = 1; // Won't trigger render

createRef

createRef is used in class components:

class TextInput extends React.Component {
  constructor(props) {
    super(props);
    this.inputRef = React.createRef();
  }

  handleClick = () => {
    this.inputRef.current.focus();
  };

  render() {
    return (
      <div>
        <input ref={this.inputRef} type="text" />
        <button onClick={this.handleClick}>Focus</button>
      </div>
    );
  }
}

Important:

Don't use createRef in functional components! A new ref will be created on every render. Use useRef.


forwardRef

forwardRef allows passing a ref through a component to its child.

Problem

function CustomInput(props) {
  return <input {...props} />;
}

// Doesn't work! ref is not forwarded
function Parent() {
  const inputRef = useRef(null);
  
  return <CustomInput ref={inputRef} />; // Error!
}

Solution

const CustomInput = forwardRef((props, ref) => {
  return <input ref={ref} {...props} />;
});

function Parent() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <div>
      <CustomInput ref={inputRef} />
      <button onClick={handleClick}>Focus</button>
    </div>
  );
}

useImperativeHandle

useImperativeHandle lets you customize what's accessible to the parent through ref.

Without useImperativeHandle

const CustomInput = forwardRef((props, ref) => {
  return <input ref={ref} />;
});

// Parent gets access to the entire DOM input

With useImperativeHandle

import { forwardRef, useImperativeHandle, useRef } from 'react';

const CustomInput = forwardRef((props, ref) => {
  const inputRef = useRef(null);

  useImperativeHandle(ref, () => ({
    // Only expose these methods
    focus: () => {
      inputRef.current.focus();
    },
    scrollIntoView: () => {
      inputRef.current.scrollIntoView();
    }
    // value, blur and other methods are not accessible
  }));

  return <input ref={inputRef} {...props} />;
});

function Parent() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus(); // Works
    // inputRef.current.value; // undefined!
  }

  return (
    <div>
      <CustomInput ref={inputRef} />
      <button onClick={handleClick}>Focus</button>
    </div>
  );
}

Practical examples

Autofocus on mount

function AutoFocusInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus();
  }, []);

  return <input ref={inputRef} />;
}

Measuring element dimensions

function MeasureComponent() {
  const divRef = useRef(null);
  const [dimensions, setDimensions] = useState({});

  useEffect(() => {
    if (divRef.current) {
      const { width, height } = divRef.current.getBoundingClientRect();
      setDimensions({ width, height });
    }
  }, []);

  return (
    <div>
      <div ref={divRef} style={{ padding: 20, backgroundColor: 'lightblue' }}>
        Measure me
      </div>
      <p>Width: {dimensions.width}px</p>
      <p>Height: {dimensions.height}px</p>
    </div>
  );
}

Third-party library integration

function VideoPlayer({ src }) {
  const videoRef = useRef(null);

  useEffect(() => {
    // Initialize player with third-party library
    const player = new ThirdPartyPlayer(videoRef.current);
    player.load(src);

    return () => {
      player.destroy();
    };
  }, [src]);

  return <video ref={videoRef} />;
}

Previous value

function usePrevious(value) {
  const ref = useRef();
  
  useEffect(() => {
    ref.current = value;
  }, [value]);
  
  return ref.current;
}

function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);

  return (
    <div>
      <p>Current: {count}</p>
      <p>Previous: {prevCount}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
}

Callback Refs

Alternative way to work with refs using callback function:

function Component() {
  const [height, setHeight] = useState(0);

  const measuredRef = useCallback(node => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height);
    }
  }, []);

  return (
    <div>
      <div ref={measuredRef}>
        <p>This div's height is {height}px</p>
      </div>
    </div>
  );
}

When NOT to use Refs

Don't use for what can be done declaratively

// Bad
function Dialog() {
  const dialogRef = useRef(null);

  function open() {
    dialogRef.current.style.display = 'block';
  }

  function close() {
    dialogRef.current.style.display = 'none';
  }

  return <div ref={dialogRef}>Dialog</div>;
}

// Good
function Dialog() {
  const [isOpen, setIsOpen] = useState(false);

  return isOpen ? <div>Dialog</div> : null;
}

Don't store data that affects rendering

// Bad
function Component() {
  const dataRef = useRef([]);

  function addItem(item) {
    dataRef.current.push(item); // Component won't re-render!
  }

  return <div>{dataRef.current.length} items</div>;
}

// Good
function Component() {
  const [data, setData] = useState([]);

  function addItem(item) {
    setData(prev => [...prev, item]); // Re-render will happen
  }

  return <div>{data.length} items</div>;
}

TypeScript typing

import { useRef, forwardRef, useImperativeHandle } from 'react';

// useRef with DOM element
function Component() {
  const inputRef = useRef<HTMLInputElement>(null);

  function handleClick() {
    inputRef.current?.focus();
  }

  return <input ref={inputRef} />;
}

// forwardRef
interface Props {
  placeholder?: string;
}

const CustomInput = forwardRef<HTMLInputElement, Props>((props, ref) => {
  return <input ref={ref} {...props} />;
});

// useImperativeHandle
interface CustomInputHandle {
  focus: () => void;
  reset: () => void;
}

const CustomInput = forwardRef<CustomInputHandle, Props>((props, ref) => {
  const inputRef = useRef<HTMLInputElement>(null);

  useImperativeHandle(ref, () => ({
    focus() {
      inputRef.current?.focus();
    },
    reset() {
      if (inputRef.current) {
        inputRef.current.value = '';
      }
    }
  }));

  return <input ref={inputRef} {...props} />;
});

Common mistakes

Accessing ref.current before mounting

// Wrong
function Component() {
  const ref = useRef(null);
  console.log(ref.current); // null! Element not created yet

  return <div ref={ref}>Hello</div>;
}

// Correct
function Component() {
  const ref = useRef(null);

  useEffect(() => {
    console.log(ref.current); // Element is accessible
  }, []);

  return <div ref={ref}>Hello</div>;
}

Creating new ref on every render

// Wrong
function Component() {
  const ref = createRef(); // New ref on every render!
  
  return <div ref={ref}>Hello</div>;
}

// Correct
function Component() {
  const ref = useRef(null); // Same ref
  
  return <div ref={ref}>Hello</div>;
}

Conclusion

Refs in React:

  • useRef — for functional components
  • createRef — for class components
  • forwardRef — for forwarding ref through component
  • useImperativeHandle — for controlling ref access
  • Don't trigger re-render on change
  • Use when declarative approach is impossible
  • Avoid for what can be done through state

In interviews:

Important to be able to:

  • Explain what refs are and when to use them
  • Show difference between useRef and useState
  • Explain what forwardRef and useImperativeHandle are for
  • Give examples of proper ref usage
  • Describe when NOT to use refs