Back to the Library

React ref Is Always Null on a Custom Component - forwardRef Explained

5 min read
“useRef(null), pass it down, call .focus() or .click() on it later - and it does nothing, because .current is still null. No error. No warning. The ref was never attached to anything, because the component it was passed to never asked for it.”

In This Post

`ref` Is Not a Normal PropWatching It FailThe Fix: forwardRefSame Bug, Different SymptomPractice These Patterns

Practice This Pattern

Blue Belt

Button Ignores Keyboard Shortcut

Pressing Enter should trigger a custom button via a ref, but btnRef.current is null - the button component is a plain function component, and React silently drops the ref instead of attaching it.

+25 KI
Enter the Dojo
Blue Belt

Search Input Never Gains Focus

An Activate Search button is supposed to focus a custom input via a ref - the status always reads 'cannot focus' because the ref never reaches the underlying <input> element.

+25 KI
Enter the Dojo
BugDojo
BlogFAQ

© 2026. Carved in code.

const btnRef = useRef(null). Pass ref={btnRef} to a component. Later, call btnRef.current.focus() or btnRef.current.click().

btnRef.current is null. Still. After the component has mounted, after the button is visibly on screen, after everything looks like it should be wired up correctly.

No error gets thrown for this - until you try to call a method on null, at which point the error is about null, not about refs, which sends a lot of debugging time in the wrong direction.

ref Is Not a Normal Prop

Every other prop you pass to a component - onClick, label, value, anything - arrives in that component's props object, and the component decides what to do with it. ref does not work this way. React intercepts ref before it would become a prop, and by default, for a plain function component, there is nowhere for React to put it. So it's dropped.

Silently, as far as your UI is concerned. There's no thrown error, nothing in the rendered output changes, no visible sign that anything went wrong. In development you may catch a console note about it, but in production there is nothing - and either way, the error message when you eventually call .click() or .focus() on null points at the call site, not at the missing forwardRef. The ref object itself - btnRef - is unaffected; it's still a valid ref, still pointing at null, because nothing ever called btnRef.current = someElement.

Watching It Fail

function ActionButton({ onClick, children }) {
  return <button onClick={onClick}>{children}</button>;
}
 
export default function ShortcutForm() {
  const [log, setLog] = useState([]);
  const btnRef = useRef(null);
 
  useEffect(() => {
    function handleKey(e) {
      if (e.key === "Enter") {
        if (btnRef.current) {
          btnRef.current.click();
        } else {
          setLog((prev) => [...prev, "ref is null - shortcut failed"]);
        }
      }
    }
    window.addEventListener("keydown", handleKey);
    return () => window.removeEventListener("keydown", handleKey);
  }, []);
 
  function handleAction() {
    setLog((prev) => [...prev, "Action fired"]);
  }
 
  return (
    <div>
      <ActionButton ref={btnRef} onClick={handleAction}>
        Run Action
      </ActionButton>
      <ul>{log.map((entry, i) => <li key={i}>{entry}</li>)}</ul>
    </div>
  );
}

<ActionButton ref={btnRef} onClick={handleAction}> looks completely normal. Click the button with a mouse - handleAction fires, "Action fired" is logged. The onClick prop works fine, because onClick is a normal prop; ActionButton receives it and passes it to the native <button>.

Press Enter anywhere on the page. The keydown listener fires, checks btnRef.current - and it's null. The else branch runs: "ref is null - shortcut failed."

ActionButton is a plain function component: function ActionButton({ onClick, children }) { ... }. It has no second parameter for a ref, and no mechanism to receive one. React saw ref={btnRef} on <ActionButton>, recognized that ActionButton can't accept it, and dropped it - btnRef.current was never set to anything, so it stays at its initial value of null.

Symptom

Clicking the button directly works perfectly - any prop-based interaction is fine. Anything that depends on ref.current - here, a keyboard shortcut calling .click() programmatically - hits null and either does nothing or falls into an explicit "ref is null" error path, with no React warning pointing at the cause.

The Fix: forwardRef

const ActionButton = forwardRef(function ActionButton({ onClick, children }, ref) {
  return (
    <button ref={ref} onClick={onClick}>
      {children}
    </button>
  );
});

forwardRef wraps the component and changes its signature: instead of just (props), the inner function receives (props, ref). React now knows what to do with a ref passed to <ActionButton> - it hands it to this second argument, and the component is responsible for attaching it to something real, here the native <button> via ref={ref}.

Now btnRef.current is the actual <button> DOM element. btnRef.current !== null, so btnRef.current.click() runs, which fires the button's onClick - handleAction - the same as a real mouse click would.

The Sensei's Hint

forwardRef doesn't change what the component renders or how its other props behave - onClick, children, everything else works exactly as before. It only changes what happens to a ref passed to this component: from "silently dropped" to "passed through to whatever native element you attach it to."

Same Bug, Different Symptom

"Search Input Never Gains Focus" is the same fix applied to a different goal - calling .focus() instead of .click():

function SearchInput({ placeholder }) {
  return <input placeholder={placeholder} />;
}
function handleActivate() {
  if (inputRef.current) {
    inputRef.current.focus();
    setStatus("active");
  } else {
    setStatus("cannot focus");
  }
}

Same shape: SearchInput is a plain function component, ref={inputRef} is dropped, inputRef.current is null, handleActivate always falls into the else branch and reports "cannot focus" - even though the <input> is right there on screen and a real DOM .focus() call would work fine on it if the ref actually pointed to it.

const SearchInput = forwardRef(function SearchInput({ placeholder }, ref) {
  return <input ref={ref} placeholder={placeholder} />;
});

Same fix: wrap with forwardRef, attach the forwarded ref to the <input>. inputRef.current becomes the real input element, .focus() works, and status correctly reads "active."

Constraints

Any time you write <SomeComponent ref={someRef} /> where SomeComponent is your own function component (not a built-in element like <input> or <div>), check that SomeComponent is wrapped in forwardRef and explicitly attaches that ref somewhere. If it's a plain function SomeComponent(props) { ... }, the ref has nowhere to go - and someRef.current will be null for the component's entire lifetime, with no error telling you why.

Practice These Patterns

"Button Ignores Keyboard Shortcut" sets up the failure clearly: the log literally prints "ref is null - shortcut failed" when you press Enter, which is as direct a diagnostic as this bug ever gives you. "Search Input Never Gains Focus" is the same root cause with a quieter symptom - a status label reading "cannot focus" instead of an explicit ref-is-null message - useful for recognizing the pattern when the code doesn't spell it out for you.