React 19 New Features Guide 2026 — Actions, useOptimistic, use() and More

Sanjeev SharmaSanjeev Sharma
4 min read

Advertisement

Introduction

Why This Matters

React 19 ships the most significant API changes since hooks arrived in 16.8. It solves recurring pain points — manual loading states, optimistic updates, async data reading — with purpose-built primitives that reduce boilerplate by 50–70% in real applications.

Actions and useActionState

Before React 19, every form needed manual isPending, error, and try/catch state. Actions collapse this into one hook:

import { useActionState } from 'react'
 
// React 18 pattern (verbose):
function OldForm() {
  const [error, setError] = useState(null)
  const [isPending, setIsPending] = useState(false)
 
  async function handleSubmit(e) {
    e.preventDefault()
    setIsPending(true)
    try {
      await submitForm(new FormData(e.target))
    } catch (err) {
      setError(err.message)
    } finally {
      setIsPending(false)
    }
  }
 
  return <form onSubmit={handleSubmit}>...</form>
}
 
// React 19 pattern (concise):
function NewForm() {
  const [state, formAction, isPending] = useActionState(
    async (prevState: any, formData: FormData) => {
      try {
        await submitForm(formData)
        return { success: true, error: null }
      } catch (err) {
        return { success: false, error: err.message }
      }
    },
    { success: false, error: null }
  )
 
  return (
    <form action={formAction}>
      {state.error && <p className="text-red-500">{state.error}</p>}
      <input name="email" type="email" required />
      <button disabled={isPending}>
        {isPending ? 'Submitting…' : 'Submit'}
      </button>
    </form>
  )
}

useOptimistic: Instant UI Feedback

useOptimistic applies a temporary state change immediately, then reverts or confirms when the server responds:

'use client'
import { useOptimistic, useActionState } from 'react'
 
interface Message {
  id: string
  text: string
  pending?: boolean
}
 
function MessageList({ initialMessages }: { initialMessages: Message[] }) {
  const [optimisticMessages, addOptimistic] = useOptimistic(
    initialMessages,
    (state: Message[], newMessage: Message) => [...state, newMessage]
  )
 
  const [, formAction] = useActionState(
    async (_: any, formData: FormData) => {
      const text = formData.get('text') as string
      addOptimistic({ id: crypto.randomUUID(), text, pending: true })
      await sendMessage(text)
    },
    null
  )
 
  return (
    <>
      <ul>
        {optimisticMessages.map(msg => (
          <li key={msg.id} className={msg.pending ? 'opacity-50' : 'opacity-100'}>
            {msg.text} {msg.pending && '(sending…)'}
          </li>
        ))}
      </ul>
      <form action={formAction}>
        <input name="text" placeholder="Type a message" />
        <button type="submit">Send</button>
      </form>
    </>
  )
}

The use() Hook

use() reads a Promise or Context during render, suspending automatically until resolved:

import { use, Suspense } from 'react'
 
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise)  // suspends until resolved
 
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  )
}
 
function Page() {
  const userPromise = fetchUser(1)  // created outside render
 
  return (
    <Suspense fallback={<Skeleton />}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  )
}
 
// use() also works for Context — and can be called conditionally
function ThemedButton() {
  const theme = use(ThemeContext)
  return <button className={theme.button}>Click me</button>
}

ref as a Prop

forwardRef is no longer needed — ref is now a standard prop:

// React 19: ref is just a prop
function Input({ ref, ...props }: React.ComponentProps<'input'>) {
  return <input ref={ref} {...props} className="border rounded px-3 py-2" />
}
 
function Form() {
  const inputRef = useRef<HTMLInputElement>(null)
 
  return (
    <form onSubmit={() => inputRef.current?.focus()}>
      <Input ref={inputRef} type="text" name="username" />
      <button type="submit">Submit</button>
    </form>
  )
}

Document Metadata Hoisting

Title, meta, and link tags placed inside components are automatically hoisted to <head>:

function BlogPost({ post }: { post: Post }) {
  return (
    <article>
      <title>{post.title} | My Blog</title>
      <meta name="description" content={post.excerpt} />
      <link rel="canonical" href={`https://example.com/blog/${post.slug}`} />
 
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  )
}

Common Mistakes

  • Creating the Promise passed to use() inside the component — this re-creates it on every render
  • Using useEffect + useState for async data when use() + Suspense is simpler
  • Forgetting to wrap use() consumers in a <Suspense> boundary
  • Keeping forwardRef wrappers after upgrading — they still work but add dead code
  • Not providing an initial state to useActionState that matches the returned shape

Best Practices

  • Use useActionState for all form submissions — it handles pending, error, and success in one hook
  • Pair useOptimistic with useActionState for chat, likes, or any list mutation
  • Create Promises at the page or layout level, then pass them to child components for use()
  • Use useTransition with async functions when you need to control pending state outside a form
  • Replace Context.Provider with Context directly (React 19 supports it)

Key Takeaways

  • useActionState replaces manual isPending + error state for async form submissions
  • useOptimistic applies an immediate optimistic state and reverts it if the server call fails
  • use() suspends a component while a Promise resolves, eliminating useEffect data-fetching patterns
  • use() can be called conditionally unlike any other hook
  • React 19 accepts ref as a plain prop — forwardRef is no longer needed for new components
  • title, meta, and link tags inside components are automatically hoisted to <head>
  • useTransition now accepts async functions directly in React 19
  • Three new root-level error callbacks (onCaughtError, onUncaughtError, onRecoverableError) improve observability

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading