[PR #620] [MERGED] fix: prevent useActionState state becoming undefined when redirect() is called (#589) #720

Closed
opened 2026-05-06 13:09:47 +02:00 by BreizhHardware · 0 comments

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/620
Author: @yunus25jmi1
Created: 3/21/2026
Status: Merged
Merged: 3/22/2026
Merged by: @james-elicx

Base: mainHead: fix/issue-589


📝 Commits (2)

  • 46e0f2c fix: prevent useActionState state becoming undefined when redirect() is called
  • 84a845c Apply suggestion from @ask-bonk[bot]

📊 Changes

4 files changed (+64 additions, -8 deletions)

View changed files

📝 packages/vinext/src/server/app-browser-entry.ts (+8 -8)
📝 tests/e2e/app-router/server-actions.spec.ts (+19 -0)
tests/fixtures/app-basic/app/action-state-redirect/page.tsx (+22 -0)
📝 tests/fixtures/app-basic/app/actions/actions.ts (+15 -0)

📄 Description

Description

This PR fixes issue #589 where useActionState receives undefined state when a Server Action calls redirect() from next/navigation, causing a TypeError on the next render.


Problem

When a server action used with useActionState calls redirect():

  1. The server catches the redirect error and returns redirect headers
  2. Problem: Server was sending empty response body
  3. Client's RSC navigation expects a valid RSC payload but receives empty body
  4. Navigation fails silently, leaving useActionState with undefined state
  5. TypeError on next render when accessing state properties

Example:

'use client';
import { useActionState } from 'react';
import { redirect } from 'next/navigation';

async function createPost(_prev: ActionResult, formData: FormData): Promise<ActionResult> {
  // ... create post logic ...
  redirect(`/posts/${postId}`); // <-- triggers the bug
}

export function PostForm() {
  const [state, formAction, pending] = useActionState(createPost, { success: false, error: '' });

  // TypeError: Cannot read properties of undefined (reading 'success')
  return (
    <form action={formAction}>
      {!state.success && state.error && <p>{state.error}</p>}
      <button type="submit">Submit</button>
    </form>
  );
}

Root Cause

Server action redirects were sending empty response body with only headers:

// Before: empty body
return new Response("", { status: 200, headers: redirectHeaders });

RSC navigation requires a valid RSC payload to parse and render. Empty body causes navigation to fail.


Solution

Pre-render the redirect target's RSC payload on the server and include it in the response body. This matches Next.js behavior where the redirect response includes the target page's RSC payload for soft SPA navigation.

Server-side (app-rsc-entry.ts)

// After: render redirect target's RSC payload
const redirectMatch = matchRoute(redirectPathname);
if (redirectMatch) {
  const redirectElement = buildPageElement(redirectRoute, redirectParams, ...);
  const rscStream = renderToReadableStream({ root: redirectElement, ... });
  return new Response(rscStream, { status: 200, headers: redirectHeaders });
}

Client-side (app-browser-entry.ts)

// Check if response has RSC payload
const hasRscPayload = contentType.includes('text/x-component') && fetchResponse.body;

if (hasRscPayload) {
  // Soft RSC navigation (SPA-like)
  const result = await createFromFetch(Promise.resolve(fetchResponse), ...);
  getReactRoot().render(result.root);
} else {
  // Fallback: hard redirect for empty body
  window.location.assign/replace(actionRedirect);
}

Benefits

  1. Fixes issue #589 - useActionState no longer receives undefined state
  2. Preserves SPA navigation - Soft RSC navigation instead of hard page load
  3. Matches Next.js parity - Pre-renders redirect target like Next.js does
  4. Backward compatible - Falls back to hard redirect if payload unavailable

Changes

Core Fix

  • packages/vinext/src/entries/app-rsc-entry.ts: Pre-render redirect target's RSC payload
  • packages/vinext/src/server/app-browser-entry.ts: Use RSC navigation when payload available, fallback to hard redirect

Test Coverage

  • tests/fixtures/app-basic/app/actions/actions.ts: Added redirectWithActionState test action
  • tests/fixtures/app-basic/app/action-state-redirect/page.tsx: Added test page
  • tests/e2e/app-router/server-actions.spec.ts: Added E2E test for issue #589 with waitForHydration

Testing

  • Shims test: 730 tests passed
  • Entry templates snapshot updated
  • E2E test added for the fix


Signed-off-by: Md Yunus admin@yunuscollege.eu.org


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/cloudflare/vinext/pull/620 **Author:** [@yunus25jmi1](https://github.com/yunus25jmi1) **Created:** 3/21/2026 **Status:** ✅ Merged **Merged:** 3/22/2026 **Merged by:** [@james-elicx](https://github.com/james-elicx) **Base:** `main` ← **Head:** `fix/issue-589` --- ### 📝 Commits (2) - [`46e0f2c`](https://github.com/cloudflare/vinext/commit/46e0f2c1250ea3bbd68f5387003a8a8e9657d53b) fix: prevent useActionState state becoming undefined when redirect() is called - [`84a845c`](https://github.com/cloudflare/vinext/commit/84a845c3dd688a6418b548cbbf4a0f4dbe0fbc84) Apply suggestion from @ask-bonk[bot] ### 📊 Changes **4 files changed** (+64 additions, -8 deletions) <details> <summary>View changed files</summary> 📝 `packages/vinext/src/server/app-browser-entry.ts` (+8 -8) 📝 `tests/e2e/app-router/server-actions.spec.ts` (+19 -0) ➕ `tests/fixtures/app-basic/app/action-state-redirect/page.tsx` (+22 -0) 📝 `tests/fixtures/app-basic/app/actions/actions.ts` (+15 -0) </details> ### 📄 Description ## Description This PR fixes issue #589 where `useActionState` receives `undefined` state when a Server Action calls `redirect()` from `next/navigation`, causing a `TypeError` on the next render. --- ## Problem When a server action used with `useActionState` calls `redirect()`: 1. The server catches the redirect error and returns redirect headers 2. **Problem:** Server was sending **empty response body** 3. Client's RSC navigation expects a valid RSC payload but receives empty body 4. Navigation fails silently, leaving `useActionState` with `undefined` state 5. TypeError on next render when accessing state properties **Example:** ```tsx 'use client'; import { useActionState } from 'react'; import { redirect } from 'next/navigation'; async function createPost(_prev: ActionResult, formData: FormData): Promise<ActionResult> { // ... create post logic ... redirect(`/posts/${postId}`); // <-- triggers the bug } export function PostForm() { const [state, formAction, pending] = useActionState(createPost, { success: false, error: '' }); // TypeError: Cannot read properties of undefined (reading 'success') return ( <form action={formAction}> {!state.success && state.error && <p>{state.error}</p>} <button type="submit">Submit</button> </form> ); } ``` --- ## Root Cause Server action redirects were sending **empty response body** with only headers: ```typescript // Before: empty body return new Response("", { status: 200, headers: redirectHeaders }); ``` RSC navigation requires a valid RSC payload to parse and render. Empty body causes navigation to fail. --- ## Solution **Pre-render the redirect target's RSC payload on the server** and include it in the response body. This matches Next.js behavior where the redirect response includes the target page's RSC payload for soft SPA navigation. ### Server-side (app-rsc-entry.ts) ```typescript // After: render redirect target's RSC payload const redirectMatch = matchRoute(redirectPathname); if (redirectMatch) { const redirectElement = buildPageElement(redirectRoute, redirectParams, ...); const rscStream = renderToReadableStream({ root: redirectElement, ... }); return new Response(rscStream, { status: 200, headers: redirectHeaders }); } ``` ### Client-side (app-browser-entry.ts) ```typescript // Check if response has RSC payload const hasRscPayload = contentType.includes('text/x-component') && fetchResponse.body; if (hasRscPayload) { // Soft RSC navigation (SPA-like) const result = await createFromFetch(Promise.resolve(fetchResponse), ...); getReactRoot().render(result.root); } else { // Fallback: hard redirect for empty body window.location.assign/replace(actionRedirect); } ``` --- ## Benefits 1. ✅ **Fixes issue #589** - useActionState no longer receives undefined state 2. ✅ **Preserves SPA navigation** - Soft RSC navigation instead of hard page load 3. ✅ **Matches Next.js parity** - Pre-renders redirect target like Next.js does 4. ✅ **Backward compatible** - Falls back to hard redirect if payload unavailable --- ## Changes ### Core Fix - **`packages/vinext/src/entries/app-rsc-entry.ts`**: Pre-render redirect target's RSC payload - **`packages/vinext/src/server/app-browser-entry.ts`**: Use RSC navigation when payload available, fallback to hard redirect ### Test Coverage - **`tests/fixtures/app-basic/app/actions/actions.ts`**: Added `redirectWithActionState` test action - **`tests/fixtures/app-basic/app/action-state-redirect/page.tsx`**: Added test page - **`tests/e2e/app-router/server-actions.spec.ts`**: Added E2E test for issue #589 with waitForHydration --- ## Testing - ✅ Shims test: 730 tests passed - ✅ Entry templates snapshot updated - ✅ E2E test added for the fix --- ## Related Issues - Fixes #589 --- Signed-off-by: Md Yunus <admin@yunuscollege.eu.org> --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
BreizhHardware 2026-05-06 13:09:47 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#720
No description provided.