React 19.1 useActionState Pending Stuck After Redirect

React 19.1 useActionState pending flag stays true after a server action redirect in Next.js 16? Here is the cause and a reliable workaround that resets it.
ReactNext.jsServer Actions
June 5, 20265 min read962 words

The Problem

A checkout button that says "Placing order…" forever was the bug on a client headless store last week. The form used useActionState to track a server action that creates the order and redirects to /checkout/success/[id]. On every successful order, the new page loaded, but the submit button on the form stayed disabled with the pending label visible behind it, and a second click did nothing.

'use client'

import { useActionState } from 'react'
import { placeOrder } from './actions'

export function CheckoutButton() {
  const [state, formAction, isPending] = useActionState(placeOrder, null)

  return (
    <form action={formAction}>
      {/* hidden fields */}
      <button disabled={isPending} type="submit">
        {isPending ? 'Placing order…' : 'Place order'}
      </button>
    </form>
  )
}

The server action ended with a redirect() call from next/navigation. Manual page refresh fixed the stuck state. Going back in history brought it back. In dev mode it sometimes recovered on its own; in production it never did. React DevTools showed useActionState permanently returning isPending: true for the unmounted-then-remounted form instance.

Why It Happens

The bug sits at the intersection of three things.

  1. redirect() in a server action throws a special NEXT_REDIRECT error. React 19 treats that throw as the action settling, but the pending-state reset only happens after the action result is committed into the same fibre.
  2. With Next.js 16 partial prerendering enabled, the form lives in a persistent shell that does not remount on the redirect. React reconciles into the same fibre instead of mounting a fresh one.
  3. React 19.1 moved useActionState pending tracking into a per-form transition that is cleared only when the next reconciliation passes a non-pending action result. The thrown redirect does not deliver a result. It triggers navigation instead, so the transition never receives a clean settle signal.

The transition that owns the pending flag stays open, the fibre survives the navigation, and isPending stays true forever. The React 19.1 release notes acknowledge the issue under "Known issues with server action redirects" and point at a fix that has not landed in the version most teams are on.

The same form in the Pages Router does not show this because the Pages Router fully unmounts on navigation. App Router with PPR does not, which is what makes the bug specific to the current Next.js 16 + React 19.1 stack.

The Fix

Two reliable workarounds. Pick by what your form needs.

Workaround 1: Drive pending from useTransition instead of useActionState. The transition hook resets correctly even when the action throws a redirect, because transition completion is tied to render commits, not action results:

'use client'

import { useTransition } from 'react'
import { placeOrder } from './actions'

export function CheckoutButton() {
  const [isPending, startTransition] = useTransition()

  return (
    <form
      action={(formData) => {
        startTransition(async () => {
          await placeOrder(formData)
        })
      }}
    >
      <button disabled={isPending} type="submit">
        {isPending ? 'Placing order…' : 'Place order'}
      </button>
    </form>
  )
}

Trade-off: you lose the typed state return from useActionState. If your form does not need to render server-validation errors, this is the cleanest fix. The pending flag flips to false the moment the new route commits.

Workaround 2: Return a redirect target from the action instead of throwing. Keep useActionState for its return value, but navigate from the client after the action returns:

// app/checkout/actions.ts
'use server'

export async function placeOrder(
  _prev: { redirectTo?: string; error?: string } | null,
  formData: FormData,
): Promise<{ redirectTo?: string; error?: string }> {
  try {
    const orderId = await createOrder(formData)
    return { redirectTo: `/checkout/success/${orderId}` }
  } catch (err) {
    return { error: err instanceof Error ? err.message : 'Unknown error' }
  }
}
'use client'

import { useActionState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { placeOrder } from './actions'

export function CheckoutButton() {
  const router = useRouter()
  const [state, formAction, isPending] = useActionState(placeOrder, null)

  useEffect(() => {
    if (state?.redirectTo) {
      router.push(state.redirectTo)
    }
  }, [state, router])

  return (
    <form action={formAction}>
      <button disabled={isPending} type="submit">
        {isPending ? 'Placing order…' : 'Place order'}
      </button>
      {state?.error && <p role="alert">{state.error}</p>}
    </form>
  )
}

Because the action returns normally instead of throwing, useActionState receives a settle and flips isPending to false before the client-side router.push runs. The user sees the same flow; the button reaches a stable idle state in between.

Do not wrap the action call in a try/catch on the client to swallow the redirect error. The NEXT_REDIRECT symbol is internal and catching it breaks the navigation entirely, which is a different bug nobody wants to debug at 2am.

The Lesson

useActionState and redirect() together are a sharp edge in the current React 19.1 + Next.js 16 stack. Either avoid the combination by driving pending from useTransition, or avoid throwing the redirect by returning the destination and navigating from the client. The framework will eventually fix the pending-state leak, but until the fix ships and your project is on it, treat this as a structural choice rather than a one-off bug.

If your Next.js form flows hang on phantom pending states after a server action upgrade, that is a project I get paid to untangle. See my services. For a related server-action regression after the same upgrade, read React useFormStatus pending stuck.

Stuck on a Next.js 16 form regression? Get it shipped.

Back to blogStart a project