Skip to content

Bug: useActionState optimization too eager #33343

@gcb

Description

@gcb

if the previous state parameter still exists when the server method exists, react will assume it was returned and ignore any data actually returned by the method.

React version:

$ npm list --depth 1 
├─┬ @types/react-dom@19.1.5
│ └── @types/react@19.1.5 deduped
├─┬ next@15.3.2
...
├─┬ @types/react@19.1.5
│ ├── eslint-plugin-react-hooks@5.2.0
│ ├── eslint-plugin-react@7.37.5
│ ├── UNMET OPTIONAL DEPENDENCY babel-plugin-react-compiler@*
│ ├── react-dom@19.1.0 deduped
│ ├── react@19.1.0 deduped
├─┬ react-dom@19.1.0
│ ├── react@19.1.0 deduped
├── react@19.1.0

Steps To Reproduce

  1. on client component const [serverState, formAction, isPending] = useActionState(myServerAction, initialState);
  2. on server action export async function myServerAction( prevState: MyData, post: FormData ): Promise<MyData> { const newState = prevState; ...and then update newState ... }

Link to code example:

See example code, started with npx create-next-app@latest today, at https://github.com/gcb/react_test_case_useActionState/tree/main/app

relevant files app/client-form.tsx and app/server-action.ts

The current behavior

if server action return any value that was copied "as reference" (no need to get religious on js concepts, anything that prevents GC from wiping the source value counts), the server action (optimization?) will return no changes to the client.

for example 1,

export async function myServerAction( prevState: MyData, post: FormData ): Promise<MyData> {
	const newState = prevState; // bug. any change to object will be ignored.

	newState.message = "hello from server";
	console.log(newState);
	return newState;
}

server console will show:

{ ... message: "hello from server"; }
but http request response sent to client will have empty changes (i.e. useActionState will keep client side value between re-renders)

example 2,

export async function myServerAction( prevState: MyData, post: FormData ): Promise<MyData> {
	const newState: MyData = { message: 'server value', data: prevState.data }; 
	const newTitle = post.get('title');
	if( newTitle ){
		newState.data.title = newTitle.valueOf() as string;
	} else {
		if( !newState.data.title ) newState.data.title = 'default from server';
	}
	newState.message = "hello from server";
	console.log(newState);
	return newState;
}

here server console will show updated values for all properties, but http response will signal that only '.message' was updated.

The expected behavior

return the actual values returned by the server action.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions