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

Sanjeev SharmaSanjeev Sharma
5 min read

Advertisement

React 19: The Biggest Update Since Hooks

React 19 ships with the most significant API changes since React 16.8 introduced hooks. These features solve long-standing pain points: form handling, optimistic updates, async state, and more.

Actions: Async Transitions

React 19 introduces Actions — async functions that handle transitions automatically:

import { useActionState, useOptimistic } from 'react'

// OLD WAY (React 18):
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>
}

// NEW WAY (React 19):
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" />
      <button disabled={isPending}>
        {isPending ? 'Submitting...' : 'Submit'}
      </button>
    </form>
  )
}

useOptimistic: Instant UI Updates

'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
      const optimisticMsg = { id: crypto.randomUUID(), text, pending: true }

      // Show optimistically immediately
      addOptimistic(optimisticMsg)

      // Then send to server
      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: Read Resources in Render

import { use, Suspense } from 'react'

// use() can read Promises and Context
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  // Suspends until promise resolves
  const user = use(userPromise)

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

// Parent creates the promise and passes it down
function Page() {
  // Promise created OUTSIDE render (not in component)
  const userPromise = fetchUser(1)

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

// use() also replaces useContext() for conditional reads
function ThemedButton() {
  const theme = use(ThemeContext)  // Can be used conditionally!
  return <button className={theme.button}>Click me</button>
}

ref as a Prop (Goodbye forwardRef)

// React 19: ref is just a prop now
function Input({ ref, ...props }: React.ComponentProps<'input'>) {
  return <input ref={ref} {...props} className="border rounded px-3 py-2" />
}

// Usage
function Form() {
  const inputRef = useRef<HTMLInputElement>(null)

  return (
    <form onSubmit={() => inputRef.current?.focus()}>
      <Input ref={inputRef} type="text" />
    </form>
  )
}

// forwardRef still works for backward compat, but not needed

Document Metadata in Components

// React 19: Hoist title, meta, link tags to <head> automatically
function BlogPost({ post }: { post: Post }) {
  return (
    <article>
      {/* These get hoisted to <head> */}
      <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>
      <p>{post.content}</p>
    </article>
  )
}

Improved Error Handling

// React 19 adds onCaughtError, onUncaughtError, onRecoverableError
import { createRoot } from 'react-dom/client'

const root = createRoot(document.getElementById('root')!, {
  onCaughtError(error, errorInfo) {
    // Error caught by error boundary
    logToSentry(error, { extra: errorInfo.componentStack })
  },
  onUncaughtError(error, errorInfo) {
    // Error not caught by error boundary
    reportCriticalError(error)
  },
  onRecoverableError(error) {
    // Hydration errors that React auto-recovered from
    console.warn('Recoverable error:', error)
  },
})

root.render(<App />)

Context Without Provider

// React 19: Context.Provider is now just Context
const ThemeContext = createContext('light')

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

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

useTransition with Async

import { useTransition } from 'react'

function SearchPage() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState([])
  const [isPending, startTransition] = useTransition()

  function handleSearch(e: React.ChangeEvent<HTMLInputElement>) {
    const value = e.target.value
    setQuery(value)

    // React 19: startTransition accepts async functions
    startTransition(async () => {
      const data = await searchAPI(value)
      setResults(data)
    })
  }

  return (
    <div>
      <input value={query} onChange={handleSearch} placeholder="Search..." />
      {isPending ? (
        <div className="animate-pulse">Searching...</div>
      ) : (
        <ResultsList results={results} />
      )}
    </div>
  )
}

Migration from React 18

// 1. Update packages
// npm install react@19 react-dom@19

// 2. Replace forwardRef with ref prop
// Before:
const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => (
  <button ref={ref} {...props} />
))
// After:
const Button = ({ ref, ...props }: ButtonProps & { ref?: React.Ref<HTMLButtonElement> }) => (
  <button ref={ref} {...props} />
)

// 3. Replace useDeferredValue for async → useTransition
// 4. Replace manual loading states with useActionState
// 5. Replace optimistic updates with useOptimistic

React 19 vs React 18 Feature Comparison

FeatureReact 18React 19
Form handlingManual stateuseActionState
Optimistic UIManualuseOptimistic
Async datauseEffectuse() + Suspense
Ref forwardingforwardRefRef as prop
ContextContext.ProviderContext directly
Error trackingLimited3 new callbacks

React 19 dramatically reduces boilerplate for the most common patterns. Start using useActionState and useOptimistic on your next form — you'll never go back.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro