vercel / vercel/next.js

Client components fail to use server actions to mutate server data

Open
#60,328 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug Lazy Loading
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).

image

Steps:

  1. Enter the app/page.tsx source.
  2. Find <Nop />. It is a patch server component I wrote, which only calls a do-nothing server action nop() from actions.ts. Firstly keep it commented out.
  3. Try to click buttons. You will see no UI updates.
  4. 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.
  5. 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 /> in page.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 /> in page.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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.