Modern Web · React

React 19 + TypeScript — A Field Guide

The React 19 + TypeScript migration notes that matter: ref as a plain prop instead of forwardRef, updated types for useActionState and useFormStatus, and the @types/react upgrade path.

John Kihiu12 min read

The React 19 type changes matter more than the runtime changes for a lot of TypeScript codebases, because the biggest one — ref becoming a plain prop on function components — removes a whole category of generic gymnastics that forwardRef forced on every reusable component. Here's what actually changes when you bump @types/react.

ref is now a regular prop

Before React 19, a function component could not accept ref as a normal prop — passing one required wrapping the component in forwardRef, which meant a second generic parameter, a different call signature, and a component that couldn't be typed the same way as one without a ref. React 19 lets function components declare ref in their props type directly, and React forwards it without the wrapper.

TSX · REF AS A PLAIN PROP
// React 19 — no forwardRef needed
type TextInputProps = {
  label: string;
  ref?: React.Ref<HTMLInputElement>;
};

function TextInput({ label, ref }: TextInputProps) {
  return (
    <label>
      {label}
      <input ref={ref} />
    </label>
  );
}

// Usage is unchanged from how you'd use any other ref-accepting component:
function Form() {
  const inputRef = useRef<HTMLInputElement>(null);
  return <TextInput label="Name" ref={inputRef} />;
}

Compare that to the React 18 shape: forwardRef<HTMLInputElement, TextInputProps>((props, ref) => ...), a separate props type that couldn't include ref itself, and a component value that TypeScript represented differently from a plain function component — enough of a wrinkle that generic wrapper components (HOCs, polymorphic as prop components) often had to special-case it. forwardRef still works in React 19 for backwards compatibility, but new components don't need it.

ref is optional in the type, and can be null

Declare it as ref?: React.Ref<T>, not ref: React.Ref<T> — most callers won't pass one, and a required prop forces every call site to supply it. Inside the component, ref can be undefined, so guard before dereferencing it in an effect.

Updated types for useActionState and useFormStatus

The types shipped for useActionState are generic over the state shape and the form data, and infer reasonably well from the reducer-like action function you pass in:

TSX · useActionState TYPES
type FormState = { error: string | null };

const [state, formAction, isPending] = useActionState<FormState, FormData>(
  async (previousState, formData) => {
    const name = formData.get('name');
    if (typeof name !== 'string' || !name.trim()) {
      return { error: 'Name is required' };
    }
    await saveName(name);
    return { error: null };
  },
  { error: null } // initial state
);

useFormStatus has no generics to configure — it returns a fixed shape ({ pending, data, method, action }) describing the nearest parent <form>'s submission state, typed the same regardless of your form's own state shape. The one gotcha: it only reports something meaningful when called from a component rendered *inside* the form it's tracking, which TypeScript can't enforce for you — it'll happily type-check a useFormStatus() call in a component that isn't nested in any form, it'll just always report pending: false.

The implicit children prop is gone

React 18's types had a quirk where React.FC implicitly added a children prop to every component, whether or not the component actually used it — a common source of components silently accepting (and ignoring) children they never rendered. React 19's types remove this implicit addition; if a component's props type doesn't declare children, passing children to it is now a type error, which is a real if slightly noisy improvement — it surfaces places where a `child` usage was doing nothing.

Practical migration notes

Bump react, react-dom, @types/react, and @types/react-dom together — mismatched major versions between the runtime and its types produce confusing errors that look like React bugs but are just type drift. Expect the children change to be the noisiest one in a large codebase: search for components using React.FC without an explicit children prop and either add it explicitly or drop React.FC entirely in favor of a plain typed function, which was already the more common recommendation before React 19.

Wrapping up

The forwardRef removal is the change that actually simplifies code you write going forward — one less wrapper, one less generic parameter, components that read the same whether or not they forward a ref. The children typing tightening and the Action-related types are smaller but still worth an afternoon of triage before you consider the TypeScript side of a React 19 upgrade done.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.