React - useRef and forwardRef

Overview

Estimated time: 20–30 minutes

Refs let you hold mutable values across renders and access DOM nodes. Forwarding refs makes library components ref-friendly. Use useImperativeHandle to expose safe imperative APIs.

Try it: Focus input with useRef

View source
function FocusDemo(){
  const inputRef = React.useRef(null);
  return (
    <div>
      <input ref={inputRef} placeholder="Click the button to focus me" />
      <button onClick={() => inputRef.current && inputRef.current.focus()} style={{marginLeft:8}}>Focus</button>
    </div>
  );
}
ReactDOM.createRoot(document.getElementById('try-ref')).render(<FocusDemo />);

Try it: forwardRef + useImperativeHandle

View source
const FancyInput = React.forwardRef(function FancyInput(props, ref){
  const innerRef = React.useRef(null);
  React.useImperativeHandle(ref, () => ({
    focus: () => innerRef.current && innerRef.current.focus(),
    value: () => innerRef.current ? innerRef.current.value : ''
  }));
  return <input ref={innerRef} {...props} />;
});
function Parent(){
  const api = React.useRef(null);
  return (
    <div>
      <FancyInput ref={api} placeholder="Forwarded ref input" />
      <button onClick={() => api.current && api.current.focus()} style={{marginLeft:8}}>Focus</button>
      <button onClick={() => alert(api.current ? api.current.value() : '')} style={{marginLeft:8}}>Get Value</button>
    </div>
  );
}
ReactDOM.createRoot(document.getElementById('try-forwardref')).render(<Parent />);

Syntax primer

  • useRef(initial) stores a mutable .current that persists across renders.
  • forwardRef passes a parent ref down to a child component.
  • useImperativeHandle limits the imperative API exposed by a ref for safety.

Common pitfalls

  • Overusing refs for state—prefer state unless you must escape React’s render cycle.
  • Exposing full DOM nodes unnecessarily—prefer a narrow imperative handle.

Checks for Understanding

  1. When should you prefer state over refs?
  2. Why use useImperativeHandle instead of exposing a full DOM node?
Show answers
  1. When UI should update based on changes—state triggers re-renders; refs do not.
  2. To provide a minimal, safe imperative API and hide internal implementation details.

Exercises

  1. Create a Timer with a ref exposing start(), stop(), and reset().
  2. Expose a limited API from a complex input (e.g., clear(), focus()) using useImperativeHandle.
  3. Build a form that focuses the first invalid field using refs.