React 19 New Features — Complete Guide

Sanjeev SharmaSanjeev Sharma
4 min read

Advertisement

React 19 New Features — Complete Guide

React 19 introduces powerful features for forms, async operations, and component development, making state management and data handling more ergonomic.

useFormStatus Hook

Track form submission state:

'use client'

import { useFormStatus } from 'react-dom'

function SubmitButton() {
  const { pending } = useFormStatus()

  return (
    <button disabled={pending} type="submit">
      {pending ? 'Submitting...' : 'Submit'}
    </button>
  )
}

export function LoginForm() {
  return (
    <form action={loginAction}>
      <input name="email" type="email" required />
      <input name="password" type="password" required />
      <SubmitButton />
    </form>
  )
}

useFormState Hook

Manage form state and actions:

'use client'

import { useFormState } from 'react-dom'

async function submitForm(previousState, formData) {
  const result = await fetch('/api/submit', {
    method: 'POST',
    body: formData
  })

  return result.json()
}

export function MyForm() {
  const [state, formAction] = useFormState(submitForm, null)

  return (
    <form action={formAction}>
      <input name="username" />
      {state?.error && <p>{state.error}</p>}
      {state?.success && <p>Submitted!</p>}
      <button type="submit">Submit</button>
    </form>
  )
}

Enhanced Hooks

useOptimistic

'use client'

import { useOptimistic } from 'react'

export function TodoList() {
  const [todos, dispatch] = useReducer(todoReducer, initialTodos)
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(todos)

  async function handleAddTodo(formData) {
    const newTodo = { id: Math.random(), text: formData.get('todo') }
    addOptimisticTodo(newTodo)

    await fetch('/api/todos', {
      method: 'POST',
      body: JSON.stringify(newTodo)
    })
  }

  return (
    <div>
      {optimisticTodos.map(todo => <div key={todo.id}>{todo.text}</div>)}
    </div>
  )
}

useTransition Enhancements

'use client'

import { useTransition } from 'react'

export function SearchResults() {
  const [isPending, startTransition] = useTransition()
  const [results, setResults] = useState([])

  function handleSearch(query) {
    startTransition(async () => {
      const data = await fetch(`/api/search?q=${query}`)
        .then(r => r.json())

      setResults(data)
    })
  }

  return (
    <div>
      {isPending && <p>Searching...</p>}
      {results.map(result => <div key={result.id}>{result.title}</div>)}
    </div>
  )
}

Automatic Batching

React 19 batches updates automatically:

export function Counter() {
  const [count, setCount] = useState(0)

  // Both updates batch together, single render
  function handleClick() {
    setCount(c => c + 1)
    setCount(c => c + 1)
  }

  return (
    <div>
      {count}
      <button onClick={handleClick}>+2</button>
    </div>
  )
}

Server Components Integration

Improved Server Component handling:

// app/dashboard/page.tsx
async function DashboardData() {
  const data = await fetch('/api/dashboard')
    .then(r => r.json())

  return <div>{data}</div>
}

export default function Dashboard() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <DashboardData />
    </Suspense>
  )
}

React Compiler

Automatic memoization:

function ExpensiveComponent({ data, config }) {
  // React Compiler auto-memoizes without useMemo/useCallback
  const filtered = data.filter(d => d.type === config.type)
  return <List items={filtered} />
}

Actions

Server Actions with automatic loading:

// app/actions.ts
'use server'

export async function createItem(formData: FormData) {
  const item = await db.items.create({
    data: {
      title: formData.get('title'),
      description: formData.get('description')
    }
  })

  return item
}
'use client'

import { createItem } from '@/app/actions'
import { useActionState } from 'react'

export function CreateForm() {
  const [state, formAction, isPending] = useActionState(createItem, null)

  return (
    <form action={formAction}>
      <input name="title" required />
      <textarea name="description" required />
      <button disabled={isPending}>
        {isPending ? 'Creating...' : 'Create'}
      </button>
      {state?.error && <p>{state.error}</p>}
    </form>
  )
}

Ref Callbacks

Cleaner ref handling:

export function ImageGallery() {
  const images = []

  return (
    <div>
      {images.map(img => (
        <img
          key={img.id}
          ref={(el) => {
            if (el) console.log('Image rendered:', img.id)
          }}
          src={img.src}
        />
      ))}
    </div>
  )
}

Enhanced Context

Better context performance:

const ThemeContext = createContext()

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')

  // Automatically optimized
  const value = { theme, setTheme }

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  )
}

Real-World: Complete Form

'use client'

import { useFormState, useFormStatus } from 'react-dom'
import { createPost } from '@/app/actions'

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

export function CreatePostForm() {
  const [state, formAction] = useFormState(createPost, null)

  return (
    <form action={formAction} className="space-y-4">
      <div>
        <label htmlFor="title">Title</label>
        <input
          id="title"
          name="title"
          required
          className="w-full border p-2"
        />
        {state?.fieldErrors?.title && (
          <p className="text-red-600">{state.fieldErrors.title}</p>
        )}
      </div>

      <div>
        <label htmlFor="content">Content</label>
        <textarea
          id="content"
          name="content"
          required
          className="w-full border p-2"
        />
        {state?.fieldErrors?.content && (
          <p className="text-red-600">{state.fieldErrors.content}</p>
        )}
      </div>

      {state?.error && (
        <p className="text-red-600">{state.error}</p>
      )}

      {state?.success && (
        <p className="text-green-600">Post published!</p>
      )}

      <SubmitButton />
    </form>
  )
}

FAQ

Q: Do I need to update to React 19 immediately? A: No, React 18 is stable. Upgrade when your project needs new features.

Q: Are old hooks deprecated? A: No, all hooks remain. New hooks add convenience for common patterns.

Q: Does React 19 work with Next.js? A: Yes, create-next-app uses latest React by default.


React 19 makes building complex UIs more intuitive and performant.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro