React 19 New Hooks

Hard

React 19 introduces four new hooks that dramatically simplify form handling and async data loading. useActionState manages form submission state and pending status. useFormStatus reads the parent form's submission status without prop drilling. useOptimistic enables instant UI updates that revert on failure. The use() hook reads promises and context conditionally during render.

Interactive Visualization

Key Points

  • useActionState replaces manual useState + isPending + try/catch for form submissions
  • useFormStatus reads parent form submission status without prop drilling — the component must be a child of a <form>
  • useOptimistic provides instant UI feedback that auto-reverts when the async action settles
  • use() reads promises (suspending until resolved) and context values conditionally — not bound by Rules of Hooks
  • All four hooks integrate with Server Actions for progressive enhancement that works before JavaScript loads

Code Examples

useActionState for Form Handling

import { useActionState } from 'react'

interface FormState {
  error: string | null
  success: boolean
}

async function submitForm(
  prevState: FormState,
  formData: FormData
): Promise<FormState> {
  const email = formData.get('email') as string
  try {
    await subscribe(email)
    return { error: null, success: true }
  } catch (e) {
    return { error: (e as Error).message, success: false }
  }
}

function NewsletterForm() {
  const [state, action, isPending] = useActionState(
    submitForm,
    { error: null, success: false }
  )

  return (
    <form action={action}>
      <input name="email" type="email" required />
      {state.error && <p className="error">{state.error}</p>}
      <button disabled={isPending}>
        {isPending ? 'Subscribing...' : 'Subscribe'}
      </button>
    </form>
  )
}

useActionState takes an async action and initial state, returning [state, boundAction, isPending]. The action receives previous state and FormData, returns next state. React manages the pending flag automatically.

useOptimistic for Instant Feedback

import { useOptimistic, useActionState } from 'react'

interface Todo {
  id: string
  text: string
  done: boolean
  pending?: boolean
}

function TodoList({ todos }: { todos: Todo[] }) {
  const [optimisticTodos, addOptimistic] = useOptimistic(
    todos,
    (state: Todo[], newTodo: string) => [
      ...state,
      { id: 'temp', text: newTodo, done: false, pending: true },
    ]
  )

  async function addAction(
    prev: { error: string | null },
    formData: FormData
  ): Promise<{ error: string | null }> {
    const text = formData.get('text') as string
    addOptimistic(text)
    await createTodo(text)
    return { error: null }
  }

  const [, action] = useActionState(addAction, { error: null })

  return (
    <div>
      {optimisticTodos.map(todo => (
        <div key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>
          {todo.text}
        </div>
      ))}
      <form action={action}>
        <input name="text" />
        <button>Add</button>
      </form>
    </div>
  )
}

useOptimistic shows the new todo immediately with a pending visual indicator. When the server confirms, the real todo replaces the optimistic one. If the action fails, the optimistic entry disappears automatically.

use() for Conditional Context and Promises

import { use, Suspense, createContext } from 'react'

const ThemeContext = createContext<'light' | 'dark'>('dark')

// use() can be called conditionally (unlike useContext)
function Greeting({ showTheme }: { showTheme: boolean }) {
  if (showTheme) {
    const theme = use(ThemeContext)
    return <h1 className={theme}>Themed greeting!</h1>
  }
  return <h1>Plain greeting</h1>
}

// use() can read promises (suspends until resolved)
function UserProfile({
  userPromise,
}: {
  userPromise: Promise<{ name: string }>
}) {
  const user = use(userPromise)
  return <h2>{user.name}</h2>
}

function Page() {
  const userPromise = fetchUser('1')
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  )
}

use() is unique: it reads promises (integrating with Suspense) and context values, but unlike other hooks, it can be called conditionally. When reading a promise, the component suspends until the value resolves.

Common Mistakes

  • Calling useFormStatus outside a <form> element — it only reads status from the nearest parent form
  • Creating a new Promise on every render when using use() — the promise must be stable (created outside the component or memoized)
  • Forgetting that useOptimistic reverts based on the source prop, not the action result — the source of truth must update for the optimistic value to settle

Interview Tips

  • Explain how useActionState eliminates the useState + isPending + try/catch boilerplate pattern
  • Know that use() breaks the Rules of Hooks intentionally — it can be conditional because it uses a different internal mechanism
  • Discuss progressive enhancement: Server Actions + useActionState make forms work before JavaScript loads

Related Concepts