"Accessing element.ref was removed in React 19": what to read instead

React · Intermediate · 6 min read · published

This article was written by Claude (Anthropic) and published automatically.

What this solves: After upgrading to React 19, console warnings appear whenever code reads a ref off a JSX element. Here's why ref moved into props and how to read it safely.

What Changed

If you just upgraded and your console is full of Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release., nothing in your app is broken yet — but a contract you depended on has moved. In React 19, ref is no longer plucked out of props and stashed on the element object. It stays in props, exactly like className or onClick.

Two consequences ship together:

Ref cleanup functions also arrived in the same release: a callback ref may now return a cleanup function instead of being called again with null.

The Old Way vs The New Way

// React 18 — ref is not a prop, so you need the wrapper
import { forwardRef } from 'react';

const Input = forwardRef(function Input({ label, ...rest }, ref) {
  return (
    <label>
      {label}
      <input ref={ref} {...rest} />
    </label>
  );
});

// and library code that merged refs read them off the element
function withAnchor(child, myRef) {
  return cloneElement(child, { ref: mergeRefs(child.ref, myRef) });
}

// callback ref cleanup meant a null branch
<div ref={(node) => {
  if (node) observer.observe(node);
  else observer.disconnect();
}} />
// React 19 — ref is just a prop
function Input({ label, ref, ...rest }) {
  return (
    <label>
      {label}
      <input ref={ref} {...rest} />
    </label>
  );
}

// read it from props
function withAnchor(child, myRef) {
  return cloneElement(child, { ref: mergeRefs(child.props.ref, myRef) });
}

// callback ref returns a cleanup function
<div ref={(node) => {
  observer.observe(node);
  return () => observer.disconnect();
}} />

Why It Was Added

forwardRef existed only because of an implementation detail: createElement stripped ref (and key) out of the props object before the component ever saw them. That single carve-out produced a long tail of friction:

Making ref a plain prop deletes the carve-out. The warning you're seeing is React telling you it kept a compatibility shim on the old location so your app didn't explode mid-upgrade.

How It Works Underneath

A JSX element is a plain object: { type, key, props, ... }. The difference is purely where ref lands during element creation, and what happens when a function component is rendered.

flowchart TD
  A["JSX: <Input ref={r} label='x' />"] --> B{React version}
  B -->|18| C["createElement strips ref"]
  C --> D["element.ref = r\nelement.props = {label}"]
  D --> E{Component type}
  E -->|forwardRef wrapper| F["render(props, ref)"]
  E -->|plain function| G["ref silently dropped"]
  B -->|19| H["createElement keeps ref in props"]
  H --> I["element.props = {label, ref: r}\nelement.ref = deprecated getter"]
  I --> J["render(props) — props.ref available"]
  I -.->|any read of element.ref| W["warn: Accessing element.ref was removed"]

The getter on element.ref is defined with Object.defineProperty and warns exactly once per component in development builds. In production builds it's silent but still a getter — so code reading it won't crash today, and will once the getter is removed. For class components the ref still resolves to the instance as before; only the storage location changed.

Should You Adopt It Yet

Yes — React 19 is stable, and this specific change is the lowest-risk part of the upgrade because the old access path still works with a warning.

The real cost is in your dependency tree, not your code. Any library that clones children and merges refs — popovers, tooltips, drag-and-drop, animation wrappers, older headless UI kits — touched element.ref. If those are pinned to pre-19-aware versions, you'll get warnings you cannot fix locally, and in the worst case a ref that merges to undefined and leaves a positioned element unanchored.

Wait if: you ship a component library consumed by React 18 apps. Removing forwardRef from your public components breaks those consumers, because in React 18 a plain function component never receives ref. Keep forwardRef in published packages until your minimum supported peer is React 19.

Migration Notes

Grep for the actual reads. The warning's stack trace is the fastest path, but a sweep helps:

# element.ref reads (filter out useRef/current noise by hand)
rg -n "(child|element|el|node)\.ref\b" src
# forwardRef usage you can now unwrap
rg -n "forwardRef" src

Fix reads with a version-tolerant fallback if you still support 18:

const ref = child.props.ref ?? child.ref;

Unwrapping forwardRef can be codemodded rather than hand-edited; the official React codemod set includes a transform for this. Do it after your app is fully on 19 — a half-migrated design system where some components take props.ref and some don't is worse than either end state.

TypeScript: install @types/react@19. ElementRef is deprecated in favour of ComponentRef, RefObject<T> no longer implies | null in the same places, and ComponentPropsWithRef is now usually what you want where you previously reached for ComponentPropsWithoutRef plus a manual ref type. npx types-react-codemod@latest preset-19 ./src handles most of the mechanical churn.

Don't suppress the warning. It fires once per component and marks a genuine future breakage; silencing it just moves the failure to the release that deletes the getter.

Key takeaway: In React 19 `ref` is an ordinary prop: read `element.props.ref`, stop wrapping components in `forwardRef`, and pin library versions that still poke at `element.ref`.

Real-world challenge

You upgrade an app to React 19. Everything renders, but the console fills with `Accessing element.ref was removed in React 19. ref is now a regular prop.` — and the stack trace points into `node_modules`, not your code. One dropdown menu also stops closing on outside click because its anchor ref is null. What do you do?

1. Confirm it's a library, not you. Run the app with the React DevTools "break on warning" or add a temporary console.trace — the warning fires at the exact call site reading element.ref.

2. Grep your own source first so you don't chase the wrong thing:

rg -n "\.ref\b" src | rg -v "useRef|createRef|\.current"

3. For your code, swap the read:

// before
const existing = child.ref;
// after — works in 19, and in 18 via the props fallback
const existing = child.props.ref ?? child.ref;

4. For the library, check for a React 19-compatible release (most popover/tooltip/animation libs shipped one). If none exists, wrap the child yourself so the library never needs to clone-with-ref:

<Tooltip>
  <span ref={myRef}><Button /></span>
</Tooltip>

5. The null ref is the same bug, not a separate one: the library read element.ref, got undefined for a function component, and merged nothing. Fixing the read restores the anchor.

Don't silence the warning — it becomes a hard removal of the getter in a later release.