# React 19 forwardRef Migration: ref als Prop in TypeScript

> React 19 forwardRef Migration in TypeScript: ref als normale Prop, Typen per ComponentProps, Refs samt Cleanup zusammenführen und asChild intakt halten.

- Autor: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Veröffentlicht: 9. Okt. 2026
- Kategorie: [Next.js und Tailwind CSS](https://wingo-ui.com/de/blog/category/nextjs-tailwind)
- Lesezeit: 9 Min.
- Kanonische URL: https://wingo-ui.com/de/blog/react-19-forwardref-ref-as-prop
- Englische Fassung: https://wingo-ui.com/blog/react-19-forwardref-ref-as-prop

## Kurz gesagt

In React 19 bekommen Function Components ref als normale Prop. Du löschst also forwardRef, liest ref aus den Props (oder lässt es in ...props mitlaufen) und typisierst es mit ComponentProps oder Ref<HTMLInputElement>. forwardRef funktioniert in React 19.3 weiterhin und loggt keine Warnung, React will es aber als veraltet markieren, und eine Bibliothek, die React 18 unterstützt, muss es behalten. Sorgfalt brauchen zwei Stellen: zusammengeführte Refs, die Cleanup-Funktionen weiterreichen sollten, und asChild, wo der Ref auf das Kind-Element gehört.

Deine Komponenten stecken voller `forwardRef`-Wrapper, `displayName`-Zeilen und `ComponentPropsWithoutRef`-Typen, und React 19 sagt, dass du nichts davon mehr brauchst. Die Änderung rund um React 19 forwardRef ist auf dem Papier klein: Function Components bekommen `ref` jetzt als normale Prop. Die Arbeit steckt in den Randfällen. Props-Typen, die den Ref entfernen, Komponenten, die den DOM-Knoten selbst brauchen, und `asChild`-Wrapper, die den Ref an ein Kind weitergeben, verhalten sich ohne Wrapper alle ein bisschen anders. Dieses Tutorial migriert ein Input und einen Button, geprüft mit React 19.3 und TypeScript 5.9, und zeigt die Muster, die wir in Wingo UI nutzen. Dessen Quellcode ruft `forwardRef` kein einziges Mal auf.

## Was hat sich an forwardRef in React 19 geändert?

Function Components bekommen `ref` in ihren Props, also hat der Wrapper nichts mehr zu tun. Der [Release-Post zu React 19](https://react.dev/blog/2024/12/05/react-19#ref-as-a-prop) erklärt, dass neue Function Components `forwardRef` nicht mehr brauchen und dass React es in künftigen Versionen als veraltet markieren und entfernen wird. Die [forwardRef-Referenz](https://react.dev/reference/react/forwardRef) sagt dasselbe: nicht mehr nötig, in einem späteren Release deprecated.

Wer forwardRef ersetzen will, nimmt also einfach die `ref`-Prop. Vier Fakten bestimmen, wie du migrierst:

- **forwardRef funktioniert weiter und bleibt still.** Wir haben die Development-Builds von `react` und `react-dom` 19.3.0 nach einer Deprecation-Meldung durchsucht und keine gefunden. Alte Komponenten laufen weiter, während du migrierst.
- **Class Components haben sich nicht geändert.** Ein Ref, den du an eine Klasse übergibst, zeigt weiterhin auf die Instanz und taucht nie in ihren Props auf.
- **`element.ref` ist auf dem Rückzug.** React 19 füllt es noch, aber wer es im Development-Modus liest, bekommt die Meldung `Accessing element.ref was removed in React 19. ref is now a regular prop.` Lies stattdessen `element.props.ref`.
- **React 18 entfernt `ref` aus den Props.** Eine Bibliothek, die React 18 noch als Peer Dependency führt, muss `forwardRef` behalten. Radix tut das: `@radix-ui/react-slot` 1.3.3 hüllt seinen `Slot` in `React.forwardRef` und akzeptiert React 16.8 bis 19. Wingo UI [setzt React 19 voraus](https://wingo-ui.com/docs/installation), deshalb nehmen seine Komponenten `ref` als ganz normale Prop.

## Wie migrierst du eine Komponente von forwardRef zu ref als Prop?

Lösch den Wrapper, typisier die Props mit `ComponentProps` und lass den Ref in `...props` mitlaufen. So sieht das klassische Input im shadcn-Stil vorher aus:

```tsx
// components/ui/input.tsx (React 18)
import * as React from "react";
import { cn } from "@/lib/utils";

const Input = React.forwardRef<HTMLInputElement, React.ComponentPropsWithoutRef<"input">>(
  ({ className, type = "text", ...props }, ref) => (
    <input ref={ref} type={type} className={cn("h-10 w-full rounded-[10px] px-3", className)} {...props} />
  ),
);
Input.displayName = "Input";

export { Input };
```

Und nachher:

```tsx
// components/ui/input.tsx (React 19)
import type { ComponentProps } from "react";
import { cn } from "@/lib/utils";

export function Input({ className, type = "text", ...props }: ComponentProps<"input">) {
  return (
    <input
      data-slot="input"
      type={type}
      className={cn("h-10 w-full rounded-[10px] px-3", className)}
      {...props}
    />
  );
}
```

`ComponentProps<"input">` enthält bereits `ref?: Ref<HTMLInputElement>`. Der Spread gibt ihn also an das `<input>` weiter, und `<Input ref={inputRef} />` funktioniert ohne eine einzige Zeile speziell für den Ref. Eine benannte Funktion liefert den React DevTools ihren Namen, also fällt auch `displayName` weg.

Für einen ganzen Ordner startest du mit dem offiziellen Codemod aus [reactjs/react-codemod](https://github.com/reactjs/react-codemod#remove-forward-ref), auf einem sauberen Branch, damit du den Diff lesen kannst:

```bash
npx codemod react/19/remove-forward-ref --target components/ui
```

shadcn/ui hat denselben Schritt gemacht, als es auf React 19 umgestiegen ist. Der [shadcn-Guide zu Tailwind v4](https://ui.shadcn.com/docs/tailwind-v4) listet die manuellen Schritte: `React.forwardRef<...>` durch `React.ComponentProps<...>` ersetzen, `ref={ref}` entfernen, ein `data-slot`-Attribut ergänzen, in eine benannte Funktion umbauen und `displayName` streichen. Der aktuelle Button dort typisiert seine Props als `React.ComponentProps<"button">` plus Varianten und `asChild`. Stecken in deinem Projekt noch ältere shadcn forwardRef-Komponenten mit lokalen Änderungen, erklärt [shadcn-Komponenten aktualisieren, ohne Änderungen zu verlieren](https://wingo-ui.com/de/blog/update-shadcn-components-without-losing-edits) den Merge.

Damit `forwardRef` nicht zurückkommt, ergänzt du eine Lint-Regel. Stand Oktober 2026 steckt [no-forward-ref von eslint-react](https://eslint-react.xyz/docs/rules/no-forward-ref) in den empfohlenen Presets, und [noReactForwardRef von Biome](https://biomejs.dev/linter/rules/no-react-forward-ref/) (seit Biome 2.2.5) ist opt-in und bringt einen als unsicher markierten Fix mit. In Paketen, die noch React 18 unterstützen, bleiben beide aus.

## Wie typisierst du ref als Prop in React 19 mit TypeScript?

Meistens musst du gar nichts typisieren, denn `ComponentProps<"button">` enthält `ref` schon. `ref?: Ref<HTMLButtonElement>` ergänzt du nur, wenn du den Props-Typ selbst schreibst. Die übrigen Typänderungen sind Löschungen und Umbenennungen:

| Muster in React 18 | Ersatz in React 19 |
|---|---|
| `forwardRef<HTMLInputElement, Props>(...)` | `ref?: Ref<HTMLInputElement>` in `Props` oder `ComponentProps<"input">` |
| `ComponentPropsWithoutRef<"input">` für einen Wrapper | `ComponentProps<"input">` (der alte Typ entfernt den Ref, den du jetzt weitergibst) |
| `ComponentPropsWithoutRef<typeof Root>` plus `ElementRef<typeof Root>` | `ComponentProps<typeof Root>`, und `ComponentRef<typeof Root>`, wenn du den Knotentyp brauchst (`ElementRef` ist deprecated) |
| `child.ref` an einem React-Element | `child.props.ref` |
| `useRef<HTMLInputElement>()` | `useRef<HTMLInputElement>(null)` (ein Argument ist jetzt Pflicht) |
| `ref={(node) => (el = node)}` | `ref={(node) => { el = node; }}` (ein Rückgabewert gilt jetzt als Cleanup) |

Über die letzte Zeile stolpern viele an dem Tag, an dem sie `@types/react` aktualisieren. Ref-Callbacks dürfen jetzt eine Cleanup-Funktion zurückgeben. Eine Arrow Function, die den Knoten implizit zurückgibt, scheitert deshalb mit `Type 'HTMLDivElement | null' is not assignable to type 'void | (() => VoidOrUndefinedOnly)'`. Setz geschweifte Klammern, und es kompiliert.

Wenn die Props deine eigenen sind, deklarierst du den Ref wie jede andere Prop:

```tsx
import type { ReactNode, Ref } from "react";

type IconButtonProps = {
  label: string;
  icon: ReactNode;
  onPress?: () => void;
  ref?: Ref<HTMLButtonElement>;
};

export function IconButton({ label, icon, onPress, ref }: IconButtonProps) {
  return (
    <button ref={ref} type="button" aria-label={label} onClick={onPress}>
      {icon}
    </button>
  );
}
```

### Generische Komponenten bekommen ihren Typparameter zurück

Am meisten profitieren generische Komponenten von ref als Prop in React 19. `forwardRef` gibt eine Komponente zurück, die nicht generisch ist. Eine typisierte Liste brauchte deshalb einen Cast, um ihr `<T>` zurückzubekommen:

```tsx
// React 18: forwardRef verliert <T>, also wird die Signatur per Cast wiederhergestellt
export const List = forwardRef(ListInner) as <T>(
  props: ListProps<T> & { ref?: Ref<HTMLUListElement> },
) => ReturnType<typeof ListInner>;
```

Jetzt ist es eine ganz normale generische Funktion. `T` wird an der Aufrufstelle inferiert, und der Ref bleibt typisiert:

```tsx
import type { ComponentProps, ReactNode } from "react";

type ListProps<T> = Omit<ComponentProps<"ul">, "children"> & {
  items: T[];
  getKey: (item: T) => string;
  renderItem: (item: T) => ReactNode;
};

export function List<T>({ items, getKey, renderItem, ...props }: ListProps<T>) {
  return (
    <ul {...props}>
      {items.map((item) => (
        <li key={getKey(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

// <List ref={listRef} items={customers} getKey={(c) => c.id} renderItem={(c) => c.name} />
```

### Wrapper-Schichten reichen den Ref ohne Extra-Code weiter

In unserem [Button](https://wingo-ui.com/components/button) taucht der Ref nie namentlich auf. `ButtonProps` ist `Omit<ComponentProps<"button">, "color" | ...>` plus unsere eigenen Props. Die Komponente liest sie über `useDefaults("button", buttonProps)`, das app-weite Defaults aus [UIProvider](https://wingo-ui.com/components/ui-config) unter die übergebenen Props legt, und spreadet den Rest auf `motion.button`. Mit `forwardRef` käme der Ref als zweites Argument an, und jede Schicht zwischen Aufrufer und Element bräuchte einen eigenen Weg, ihn mitzunehmen. Als Prop ist er einfach ein Key mehr.

## Wie führst du Refs in React 19 zusammen?

Du führst die beiden Refs in einem memoisierten Callback-Ref zusammen, der ein Cleanup zurückgibt. Refs zusammenführen musst du, wenn die Komponente den DOM-Knoten selbst nutzt und der Aufrufer ihn auch will. Unser [Input](https://wingo-ui.com/components/input) ist der echte Fall. Es braucht den Knoten, um den Text mit einem echten Input-Event zu leeren, den Fokus danach zurückzusetzen und Formular-Resets mitzubekommen. Dein Code braucht denselben Knoten, um das Feld nach einem fehlgeschlagenen Absenden zu fokussieren. Das ist das Muster aus `components/ui/input.tsx`, gekürzt auf ein Suchfeld:

```tsx
"use client";

import { useCallback, useRef, type ComponentProps, type Ref } from "react";

// gibt den Knoten an den Ref des Aufrufers und liefert das passende Cleanup zurück
function attachRef<T>(ref: Ref<T> | undefined, node: T) {
  if (typeof ref === "function") {
    const cleanup = ref(node);
    return () => (typeof cleanup === "function" ? cleanup() : ref(null));
  }
  if (ref) {
    const object = ref;
    object.current = node;
    return () => {
      object.current = null;
    };
  }
  return undefined;
}

// schreibt über den Setter des Prototyps, damit React die Änderung sieht, und feuert das Event, auf das onChange hört
function clearValue(input: HTMLInputElement) {
  Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set?.call(input, "");
  input.dispatchEvent(new Event("input", { bubbles: true }));
}

export function SearchField({ ref, ...props }: ComponentProps<"input">) {
  const inputRef = useRef<HTMLInputElement | null>(null);

  const setRef = useCallback(
    (node: HTMLInputElement | null) => {
      inputRef.current = node;
      const detach = attachRef(ref, node);
      return () => {
        inputRef.current = null;
        detach?.();
      };
    },
    [ref],
  );

  return (
    <div className="flex items-center gap-2">
      <input ref={setRef} type="search" {...props} />
      <button
        type="button"
        onClick={() => {
          if (!inputRef.current) return;
          clearValue(inputRef.current);
          inputRef.current.focus();
        }}
      >
        Leeren
      </button>
    </div>
  );
}
```

Drei Details sind wichtig:

- **Cleanups.** Wenn der Knoten verschwindet, ruft React 19 das Cleanup auf, das ein Ref-Callback zurückgegeben hat, statt den Callback noch einmal mit `null` aufzurufen. `attachRef` führt deshalb das Cleanup des Aufrufers aus, wenn er eines zurückgegeben hat, und fällt sonst auf `ref(null)` zurück.
- **Ein stabiler Callback.** `useCallback` hält `setRef` zwischen Renders gleich. Ein neuer Callback-Ref bei jedem Render lässt React ihn jedes Mal lösen und neu anhängen. In einem Test unter StrictMode hat ein Inline-Ref-Callback nach einem einfachen Re-Render sein Cleanup und ein frisches Anhängen geloggt, während der memoisierte angehängt blieb.
- **Ein Leeren, das React mitbekommt.** `clearValue` geht über den nativen Value-Setter und feuert ein `input`-Event. So läuft das `onChange` des Aufrufers mit einem leeren Wert, wie nach einem Tastendruck. Wer `input.value = ""` zuweist, leert das Feld, ohne React oder eine Formular-Bibliothek zu informieren.

[Textarea](https://wingo-ui.com/components/textarea) führt Refs genauso zusammen, damit sie ihren eigenen Knoten messen und mit dem Inhalt wachsen kann.

Von außen nutzt du es wie einen normalen Ref:

```tsx
"use client";

import { useRef } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";

export function SignupForm() {
  const emailRef = useRef<HTMLInputElement>(null);

  return (
    <form
      noValidate
      onSubmit={(event) => {
        event.preventDefault();
        const email = emailRef.current?.value.trim() ?? "";
        if (!email.includes("@")) emailRef.current?.focus();
      }}
      className="flex flex-col gap-3"
    >
      <Input ref={emailRef} type="email" label="E-Mail" placeholder="emma.carter@example.com" clearable />
      <Button type="submit">Konto erstellen</Button>
    </form>
  );
}
```

`register` aus React Hook Form gibt einen Ref auf dieselbe Weise weiter, also erreicht `{...register("email")}` das `<input>` ohne Adapter. Welche Felder stattdessen `Controller` brauchen, steht im [Guide zu React-Formular-Komponenten](https://wingo-ui.com/de/blog/react-form-components-guide).

> Live-Demo (Input): Drück Continue mit einem kurzen Titel: Der eigene Ref der Demo setzt den Fokus zurück ins Feld. Dann gib etwas ein und drück Escape oder tipp auf den Button zum Leeren: Der interne Ref des Inputs leert das Feld und behält den Fokus. Probier sie unter [Input](https://wingo-ui.com/components/input) aus und installier sie mit `npx wingo-ui@latest add input`.

Wenn du Methoden statt eines Knotens nach außen gibst, funktioniert `useImperativeHandle(ref, ...)` genau wie vorher, nur mit `ref` aus den Props. Unser Pro [Schema Builder](https://wingo-ui.com/components/schema-builder) gibt so `validate()` und `getJsonSchema()` heraus; was Pro enthält, siehst du unter [Preise](https://wingo-ui.com/de/pricing).

## Wie funktioniert ref als Prop mit asChild und Slot?

Spreade die Props samt Ref auf `Slot` und setz den Ref auf das Kind, wenn du dessen Typ brauchst. Radix `Slot` kombiniert den Ref, den es bekommt, mit dem eigenen Ref des Kindes. In React 19 liest es den Ref des Kindes aus `element.props.ref`, also geht keiner verloren, und es erscheint keine Warnung.

Die Falle ist TypeScript. Eine Button-Komponente typisiert ihren Ref für `<button>`, also scheitert ein Link-Ref am Wrapper:

```tsx
import Link from "next/link";
import { useRef } from "react";
import { Button } from "@/components/ui/button";

export function UpgradeLink() {
  const linkRef = useRef<HTMLAnchorElement>(null);

  // Fehler: Type 'RefObject<HTMLAnchorElement | null>' is not assignable to type 'Ref<HTMLButtonElement> | undefined'
  return (
    <Button asChild ref={linkRef}>
      <Link href="/pricing">Tarife ansehen</Link>
    </Button>
  );
}
```

Setz den Ref auf das Kind. Slot führt ihn mit seinem eigenen zusammen, und der Typ stimmt:

```tsx
"use client";

import Link from "next/link";
import { useEffect, useRef } from "react";
import { Button } from "@/components/ui/button";

export function UpgradeLink({ highlight }: { highlight: boolean }) {
  const linkRef = useRef<HTMLAnchorElement>(null);

  useEffect(() => {
    if (highlight) linkRef.current?.focus();
  }, [highlight]);

  return (
    <Button asChild variant="outline">
      <Link ref={linkRef} href="/pricing">
        Tarife ansehen
      </Link>
    </Button>
  );
}
```

Unser Button rendert `asChild` über `motion.create(Slot)`, also wollen zwei Refs denselben Knoten: der von motion für die Press-Feder und deiner. Slot kombiniert beide. Wir haben diesen Button in Chromium unter StrictMode mit einem einfachen Anchor als Kind gemountet: Der Ref des Kindes bekam das `<a>`, und die Konsole blieb leer.

> Live-Demo (Button): Dieser Button hat asChild aktiviert und rendert deshalb einen Link: Drück ihn, und er federt genauso ein. Spring per Tab hin, und du siehst denselben Fokusring. Probier sie unter [Button](https://wingo-ui.com/components/button) aus und installier sie mit `npx wingo-ui@latest add button`.

Zwei weitere Dinge machen `asChild` nach einer Migration kaputt:

- **Eine Kind-Komponente, die den Ref verliert.** Slot gibt `ref`, `className` und Event-Handler an das Kind weiter, und eine Komponente, die ihre Props nicht spreadet, verliert alle drei. Radix Tooltips und Popover finden ihren Trigger über diesen Ref. Ohne ihn hat der Inhalt kein Element, an dem er sich positionieren kann.
- **Ein selbstgebauter Slot, der `child.ref` liest.** Zur Laufzeit funktioniert das noch, aber im Development-Modus loggt er den `element.ref`-Fehler, und `@types/react` 19 deklariert `ref` nicht mehr auf `ReactElement`, also lehnt TypeScript es ebenfalls ab. Lies den Ref aus den Props, mit einem Typargument, weil Element-Props in React 19 standardmäßig `unknown` sind:

```tsx
import { isValidElement, type ReactNode, type Ref } from "react";

type WithRef = { ref?: Ref<HTMLElement> };

// Code für React 18 las children.ref, was in Development-Builds von React 19 einen Fehler loggt
export function getChildRef(children: ReactNode) {
  return isValidElement<WithRef>(children) ? children.props.ref : undefined;
}
```

## Was solltest du als Nächstes ausprobieren?

Installier die beiden kostenlosen Komponenten aus diesem Post und schau dir in deinem eigenen Projekt an, wie sie mit Refs umgehen:

```bash
npx wingo-ui@latest add button input
```

Beide landen als Quellcode, der dir gehört, in `components/ui`, zusammen mit Field, Spinner und den Helfern, die sie importieren, und keine dieser Dateien nutzt `forwardRef`. Für die andere Hälfte des Stacks, auf den sie ausgelegt sind, stöber in den [Guides zu Next.js und Tailwind CSS](https://wingo-ui.com/de/blog/category/nextjs-tailwind), am besten zuerst in [Tailwind v4 Design Tokens](https://wingo-ui.com/de/blog/tailwind-v4-design-tokens).

## Komponenten in diesem Artikel

- [Button](https://wingo-ui.com/components/button) (Free): The React button component every screen starts with: five variants, six tones, all sizes, and a loading state that never jumps. Installation: `npx wingo-ui@latest add button`
- [Input](https://wingo-ui.com/components/input) (Free): A React text input: icons and addons inside the box, a clear button, a loading spinner, a character counter and floating labels. Installation: `npx wingo-ui@latest add input`

## Häufige Fragen

### Ist forwardRef in React 19 deprecated?

Nicht offiziell. Laut React-Doku ist forwardRef in React 19 nicht mehr nötig und wird in einem künftigen Release als veraltet markiert. Stand React 19.3 funktioniert es noch und loggt keine Warnung, neue Function Components sollten ref aber als Prop nehmen.

### Wie typisiere ich die ref-Prop in React 19 mit TypeScript?

Wenn du ein einzelnes Element umhüllst, nimm `ComponentProps<'button'>`, das ref schon enthält. Schreibst du den Props-Typ selbst, ergänze `ref?: Ref<HTMLButtonElement>`. Verwende `ComponentPropsWithoutRef` nicht mehr für Wrapper und ersetze `ElementRef` durch `ComponentRef`.

### Brauche ich forwardRef noch, wenn meine Bibliothek React 18 unterstützt?

Ja. React 18 entfernt ref aus den Props, bevor deine Komponente läuft, dort bekommst du den Ref also nur über forwardRef. Behalte es, bis React 18 aus deinen Peer Dependencies verschwindet; forwardRef-Komponenten funktionieren auch in React 19.

### Funktioniert asChild noch ohne forwardRef?

Ja. Radix Slot liest in React 19 den Ref des Kindes aus dessen Props und kombiniert ihn mit dem Ref, den der Slot bekommt. Setz den Ref auf das Kind, wenn du dessen echten Typ brauchst, etwa HTMLAnchorElement für einen Link, der über eine Button-Komponente gerendert wird.

### Gibt es einen Codemod, der forwardRef entfernt?

Ja. Das Projekt reactjs/react-codemod liefert remove-forward-ref: Führ `npx codemod react/19/remove-forward-ref --target components/ui` auf einem sauberen Branch aus und prüf danach den Diff, vor allem generische Komponenten und Props-Typen, die noch `ComponentPropsWithoutRef` nutzen.

### Bekommen Class Components in React 19 ref als Prop?

Nein. Ein Ref, den du an eine Class Component übergibst, zeigt weiterhin auf die Instanz der Komponente und landet nicht in ihren Props.

---

Quelle: https://wingo-ui.com/de/blog/react-19-forwardref-ref-as-prop
