← Back to articles
React React 19TypeScript

React 19: What Actually Changes for Developers

· 5 min read

React 19 is the biggest release since hooks. Not because it adds flashy syntax, but because it removes entire categories of code you used to write by hand. The React Compiler kills most manual memoization. Actions replace onSubmit handlers wired to useState + useEffect. use() lets you read promises and context anywhere. And forwardRef is dead, replaced by ref as a regular prop. Each of these changes individually saves a few lines. Together, they reshape how React components are written.

The React Compiler: automatic memoization

Every React developer has wrestled with useMemo, useCallback, and React.memo. Wrap too little and you get unnecessary re-renders. Wrap too much and you get unreadable code that’s harder to maintain than the performance problem it solves. The React Compiler eliminates this tradeoff entirely.

It analyzes your components at build time and automatically inserts the memoization that matters. You write plain JavaScript, and the compiler figures out what needs to be cached.

// Before: manual memoization everywhere
const ProductCard = React.memo(function ProductCard({ product, onAdd }: Props) {
  const formattedPrice = useMemo(() => formatCurrency(product.price), [product.price]);
  const handleClick = useCallback(() => onAdd(product.id), [onAdd, product.id]);

  return (
    <div>
      <h3>{product.name}</h3>
      <span>{formattedPrice}</span>
      <button onClick={handleClick}>Add to cart</button>
    </div>
  );
});
// After: the compiler handles it
function ProductCard({ product, onAdd }: Props) {
  const formattedPrice = formatCurrency(product.price);

  return (
    <div>
      <h3>{product.name}</h3>
      <span>{formattedPrice}</span>
      <button onClick={() => onAdd(product.id)}>Add to cart</button>
    </div>
  );
}

Same performance, half the code, zero cognitive overhead. The compiler understands React’s rules (pure rendering, immutable props) and optimizes accordingly. Inline arrow functions in JSX are no longer a performance concern.

Actions and useActionState: forms without the boilerplate

Form handling in React has always been verbose. You need state for the input values, state for the loading indicator, state for errors, an async handler that ties it all together, and error boundaries or try/catch blocks around every submission. Actions collapse all of this into a single pattern.

import { useActionState } from 'react';

async function loginAction(prevState: ActionState, formData: FormData) {
  const email = formData.get('email') as string;
  const password = formData.get('password') as string;

  try {
    await api.login({ email, password });
    return { success: true, error: null };
  } catch (e) {
    return { success: false, error: 'Invalid credentials' };
  }
}

function LoginForm() {
  const [state, action, isPending] = useActionState(loginAction, {
    success: false,
    error: null,
  });

  return (
    <form action={action}>
      <input name="email" type="email" required />
      <input name="password" type="password" required />
      <button type="submit" disabled={isPending}>
        {isPending ? 'Signing in...' : 'Sign in'}
      </button>
      {state.error ? <p>{state.error}</p> : null}
    </form>
  );
}

No useState for loading. No onSubmit with preventDefault. No useEffect to reset error state. The action prop on <form> is a native React concept now, and isPending tracks the async lifecycle automatically.

useOptimistic: instant UI feedback

useOptimistic pairs with Actions to show immediate feedback before the server responds. The optimistic value resets automatically when the Action completes.

import { useOptimistic } from 'react';

function LikeButton({ count, onLike }: Props) {
  const [optimisticCount, setOptimisticCount] = useOptimistic(count);

  async function handleLike() {
    setOptimisticCount(optimisticCount + 1);
    await onLike();
  }

  return (
    <form action={handleLike}>
      <button type="submit">{optimisticCount} likes</button>
    </form>
  );
}

The count increments instantly on click. If the server call fails, React reverts to the real value. No manual rollback logic, no previous-state snapshots.

The use() hook: reading promises and context anywhere

use() is unlike any other hook because it can be called inside conditionals and loops. It reads the current value of a promise or a context, and it works with Suspense for async data.

import { use, Suspense } from 'react';

function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise);

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

function Page({ userId }: { userId: string }) {
  const userPromise = fetchUser(userId);

  return (
    <Suspense fallback={<Skeleton />}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}

The promise is created in the parent and passed down. use() suspends the component until it resolves. No useEffect, no loading state, no isLoading boolean. The Suspense boundary handles the fallback.

For context, use() replaces useContext with the added ability to read context conditionally.

import { use } from 'react';

function ThemeText({ showTheme }: { showTheme: boolean }) {
  if (showTheme) {
    const theme = use(ThemeContext);
    return <span>{theme}</span>;
  }
  return null;
}

This would throw with useContext because hooks can’t be called conditionally. use() has no such restriction.

ref as a prop: forwardRef is gone

forwardRef was always awkward. It wrapped your component in a higher-order function, broke TypeScript inference in subtle ways, and made the component harder to read. React 19 lets you accept ref as a regular prop.

// Before: forwardRef wrapper
const Input = forwardRef<HTMLInputElement, InputProps>(function Input(props, ref) {
  return <input ref={ref} {...props} />;
});
// After: ref is just a prop
function Input({ ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
  return <input ref={ref} {...props} />;
}

No wrapper, no generic gymnastics, no extra indentation. The component signature tells you everything. This is especially valuable in design system libraries where every component forwards refs.

Document metadata in components

Title, meta tags, and link elements can now be rendered directly inside components. React 19 hoists them to the <head> automatically.

function BlogPost({ post }: { post: Post }) {
  return (
    <article>
      <title>{post.title}</title>
      <meta name="description" content={post.excerpt} />
      <meta property="og:title" content={post.title} />
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  );
}

No need for react-helmet or a separate head management library. The metadata lives next to the content it describes, which is where it logically belongs.

Conclusion

React 19 removes friction at every level. The Compiler eliminates memoization busywork. Actions and useActionState simplify forms down to their essence. use() makes async data and conditional context reads natural. Dropping forwardRef cleans up component signatures across entire codebases. These aren’t experimental features behind flags. They’re stable, shipping, and ready to reduce the amount of code you maintain today.