Client components fail to use server actions to mutate server data
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 142k
- Forks
- 32.4k
- Avg merge
- 2d 14h
- Merged PRs (30d)
- 351
Description
Link to the code that reproduces this issue
https://codesandbox.io/p/devbox/next-js-server-action-issue-lxjtkg
To Reproduce
This is a demo that use buttons (client components) to call server actions (from actions.ts) to mutate server-side data array (in data.ts).
Steps:
- Enter the
app/page.tsxsource. - Find
<Nop />. It is a patch server component I wrote, which only calls a do-nothing server actionnop()fromactions.ts. Firstly keep it commented out. - Try to click buttons. You will see no UI updates.
- Check server console logs to see array length changes, but the page UI does not. If removing an item after it gets empty, an error is raised. It seems like mutating an invisible copied array.
- Now uncomment
<Nop />, and again try the buttons. You will see they work magically with UI updates.
Relevant code:
// page.tsx
import Nop from "./ui/server-nop";
import { AddItemClient, RemoveItemClient } from "./ui/client-components";
import { fetchItems } from "./lib/data";
export default async function Home() {
const data_array = await fetchItems();
return (
<>
{/* comment / uncomment the following line */}
{/* <Nop /> */}
...
<AddItemClient />
<RemoveItemClient />
<p>
Array length: <span>{data_array.length}</span>
</p>
<ul>{data_array?.map((item) => <li key={item.id}>{item.value}</li>)}</ul>
...
</>
);
}
// Nop component
import { nop } from "../lib/actions";
export default function Nop() {
nop();
return <></>;
}
// client buttons
"use client";
import { createItem, deleteItem } from "../lib/actions";
export function AddItemClient() {
return (
<form action={createItem}>
<button type="submit">Add Item (Client Component)</button>
</form>
);
}
export function RemoveItemClient() {
return (
<form action={deleteItem}>
<button type="submit">Remove Item (Client Component)</button>
</form>
);
}
// actions.ts
"use server";
import { data_in_memory } from "./data";
import { revalidatePath } from "next/cache";
export async function nop() {}
export async function createItem() {
const length_before = data_in_memory.length;
data_in_memory.push({
id: "23",
value: "item (added)",
});
const length_after = data_in_memory.length;
console.log(`createItem: array length ${length_before} -> ${length_after}`);
revalidatePath("/");
}
export async function deleteItem() {
const length_before = data_in_memory.length;
if (data_in_memory.length === 0) {
throw new Error("Cannot remove item from empty array!");
}
data_in_memory.pop();
const length_after = data_in_memory.length;
console.log(`deleteItem: array length ${length_before} -> ${length_after}`);
revalidatePath("/");
}
// data.ts
export const data_in_memory = [
{
id: "1",
value: "item 1",
},
{
id: "2",
value: "item 2",
},
];
export async function fetchItems() {
return data_in_memory;
}
Current vs. Expected behavior
Current:
- Without
<Nop />inpage.tsx: clicking buttons will not update the UI, and will modify an invisible array on server (which can be verified through server log, or clicking remove button multiple times to see an error of empty array). - With
<Nop />inpage.tsx: the buttons work as magic.
Expected: with or without the patch component <Nop />, clicking the buttons (client components) should correctly add or remove items of the correct server-side data array, and the page UI should be updated accordingly.
Verify canary release
- I verified that the issue exists in the latest Next.js canary release
Provide environment information
Operating System:
Platform: linux
Arch: x64
Version: #1 SMP PREEMPT_DYNAMIC Sun Aug 6 20:05:33 UTC 2023
Binaries:
Node: 20.9.0
npm: 9.8.1
Yarn: 1.22.19
pnpm: 8.10.2
Relevant Packages:
next: 14.0.5-canary.41
eslint-config-next: N/A
react: 18.2.0
react-dom: 18.2.0
typescript: 5.1.3
Next.js Config:
output: N/A
Which area(s) are affected? (Select all that apply)
Not sure, Dynamic imports (next/dynamic)
Which stage(s) are affected? (Select all that apply)
next dev (local), next start (local)
Additional context
Observations:
- We need a server component (like
<Nop />) to actually import something from the server action file (actions.ts) to make client components have access to the right data array in server's memory. - If not, client components will issue server actions that add to or remove from a copy of the data array, instead of the original array.
My temp solution: patch such a nop server component that imports from the server action file; or pass the server action as prop to client components.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by running the linked CodeSandbox reproduction and comparing app/page.tsx with and without the Nop component. Then inspect actions.ts and data.ts alongside the server-action and revalidation behavior. Done means client-component buttons update the displayed array correctly in both configurations without empty-array errors.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- nextjs, react, typescript
- Domain
- backend, frontend, full-stack
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100