Skip to main content

React 19: Every New Feature Developers Actually Use

April 28, 2026

</>

React 19 landed without a lot of fanfare, which is almost the point. The team spent years fixing the things that made React frustrating before adding new surface area. The result is a release that makes forms, async state, and context consumption significantly less painful.

Here is what actually changed.

Actions

The biggest shift in React 19 is Actions — a first-class way to handle async mutations from form submissions and event handlers.

Before React 19, you wired up a handleSubmit, managed isPending state yourself, caught errors manually, and remembered to reset loading state in every code path. Actions collapse that into a single function.

async function updateName(formData: FormData) {
  'use server'
  await db.user.update({ name: formData.get('name') })
}

export function NameForm() {
  return (
    <form action={updateName}>
      <input name="name" />
      <button type="submit">Save</button>
    </form>
  )
}

Pass an async function to action and React handles the pending state, error boundary integration, and optimistic update lifecycle for you.

Client-side Actions work the same way — just omit 'use server'.

useFormStatus

useFormStatus gives any component inside a form access to the form's submission state without prop drilling.

import { useFormStatus } from 'react-dom'

function SubmitButton() {
  const { pending } = useFormStatus()
  return <button disabled={pending}>{pending ? 'Saving...' : 'Save'}</button>
}

The key detail: useFormStatus reads from the parent form, not a form the component renders itself. This makes it useful for shared button components that need to reflect form state without knowing anything about the form's logic.

useOptimistic

useOptimistic lets you show an updated UI immediately while an async operation completes in the background.

const [optimisticMessages, addOptimisticMessage] = useOptimistic(
  messages,
  (state, newMessage) => [...state, { text: newMessage, sending: true }]
)

async function sendMessage(formData: FormData) {
  const text = formData.get('message') as string
  addOptimisticMessage(text)
  await api.sendMessage(text)
}

If the action succeeds, React reconciles with the real server state. If it fails, the optimistic update rolls back. You get snappy UIs without managing rollback logic yourself.

useActionState

useActionState (formerly useFormState in the canary) wires up an action to component state in one call.

const [error, submitAction, isPending] = useActionState(
  async (prevState, formData) => {
    const result = await updateUser(formData)
    if (!result.ok) return result.error
    return null
  },
  null
)

It returns the current state, a wrapped action to pass to form action, and the pending boolean. All three things you were wiring manually before.

The use() hook

use() is a new primitive that reads the value of a Promise or Context inside a component — including conditionally.

import { use } from 'react'

function Comments({ commentsPromise }) {
  const comments = use(commentsPromise)
  return comments.map(c => <p key={c.id}>{c.text}</p>)
}

Suspense catches the pending state; error boundaries catch failures. The component just reads the resolved value.

For context, use() differs from useContext in that you can call it inside conditionals and loops — useful when you only need context in certain branches.

React Compiler (experimental)

The React Compiler automatically memoizes components and hooks, removing the need for manual useMemo, useCallback, and React.memo in most cases.

It is experimental and opt-in. Add it to your Babel or Vite config and it analyses your component tree statically, inserting memoization where it is safe and effective.

Early benchmarks show meaningful performance gains in large apps. More importantly, it removes an entire category of bugs caused by incorrect dependency arrays.

It does not replace all manual optimization — profiling and targeted fixes still matter for hot paths. But it eliminates the tedious default-memoize-everything pattern most teams fall into.

ref as a prop

You can now pass ref as a plain prop without forwardRef.

function Input({ ref, ...props }) {
  return <input ref={ref} {...props} />
}

forwardRef still works but is no longer necessary. Clean component APIs without the wrapper noise.

Context as a provider

const ThemeContext = createContext('light')

function App() {
  return (
    <ThemeContext value="dark">
      <Page />
    </ThemeContext>
  )
}

<ThemeContext.Provider> still works, but you can now use the context object directly as the provider element.

Document metadata support

React 19 supports rendering <title>, <meta>, and <link> tags from anywhere in the component tree. React hoists them to <head> automatically.

function BlogPost({ post }) {
  return (
    <>
      <title>{post.title}</title>
      <meta name="description" content={post.summary} />
      <article>{post.content}</article>
    </>
  )
}

For Next.js apps using the Metadata API, this is less relevant — but for apps managing their own document head, it removes the need for react-helmet or similar libraries.

What to upgrade first

If you are on React 18, the upgrade path is smooth. The breaking changes are minimal — mostly around removed legacy APIs like string refs and legacy Context.

Start with: replace any useFormState canary imports with useActionState, remove your forwardRef wrappers, and try Actions on one form. The compiler can wait until it stabilises further.

React 19 is not a revolution. It is the version that removes the apologies.

Recommended Posts