iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

forwardRef & refs

forwardRef lets a parent get a ref to a DOM node (or imperative handle) inside a child component. React 19 made it mostly unnecessary — refs are just props now — but forwardRef still appears everywhere in older code and in libraries that support both versions. Pair it with useImperativeHandle when the parent should call methods, not poke the DOM.

forwardRef + useImperativeHandle for focus + scroll

EXAMPLE
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';

// 1) Classic forwardRef — forward the DOM ref to the underlying <input>
const FancyInput = forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
  function FancyInput(props, ref) {
    return (
      <input
        ref={ref}
        className='border rounded px-3 py-1.5 focus:ring-2 focus:ring-blue-500'
        {...props}
      />
    );
  }
);

// 2) Imperative handle — expose a SMALL named API instead of the raw DOM node
type ListHandle = { scrollToTop: () => void; selectAll: () => void };

const Editable = forwardRef<ListHandle, { items: string[] }>(
  function Editable({ items }, ref) {
    const wrap = useRef<HTMLUListElement>(null);
    const [selected, setSelected] = useState<Set<number>>(new Set());

    useImperativeHandle(ref, () => ({
      scrollToTop: () => wrap.current?.scrollTo({ top: 0, behavior: 'smooth' }),
      selectAll:   () => setSelected(new Set(items.map((_, i) => i))),
    }), [items]);

    return (
      <ul ref={wrap} className='h-64 overflow-auto'>
        {items.map((t, i) => (
          <li key={i}
              onClick={() => setSelected(new Set([i]))}
              className={selected.has(i) ? 'bg-blue-50' : ''}>{t}</li>
        ))}
      </ul>
    );
  }
);

// 3) Parent calls the methods, never touches the DOM directly
export default function Demo() {
  const inputRef = useRef<HTMLInputElement>(null);
  const listRef  = useRef<ListHandle>(null);

  return (
    <div className='p-4 space-y-3'>
      <FancyInput ref={inputRef} placeholder='Type here' />
      <button onClick={() => inputRef.current?.focus()}>Focus input</button>

      <Editable ref={listRef} items={['Apples', 'Bananas', 'Cherries', 'Dates']} />
      <button onClick={() => listRef.current?.scrollToTop()}>Scroll top</button>
      <button onClick={() => listRef.current?.selectAll()}>Select all</button>
    </div>
  );
}

// 4) React 19 simplification — refs are just props
// function FancyInput19({ ref, ...props }: { ref?: React.Ref<HTMLInputElement> } & React.InputHTMLAttributes<HTMLInputElement>) {
//   return <input ref={ref} {...props} />;
// }
// useImperativeHandle still exists; you just stop wrapping in forwardRef.

// 5) Patterns to avoid
// - Returning the raw DOM node from a useImperativeHandle that exposes 'getDOM()'.
//   If the parent needs the DOM, give them a ref to it directly. Imperative
//   handles should expose intent ('focus', 'open'), not the implementation.
// - Forgetting the dependency array on useImperativeHandle — the handle then
//   captures stale state via closure.
// - forwardRef on a component that does not actually use the ref. Drop it; it
//   adds noise and forces every caller to think about refs that go nowhere.

Why it matters

Use useImperativeHandle to expose intent (focus, open, reset) instead of the underlying DOM node. The parent depends on a small named API, the child can rewrite its internals without breaking callers, and refactors stay local — the opposite of what happens when every parent reaches in for the same DOM property.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
const Input = forwardRef((props, ref) => <input ref={ref} {...props} />);
Try it Yourself »

Discussion

Loading…