vercel / next.js

The React Framework
https://nextjs.org
MIT License
125.15k stars 26.73k forks source link

Client components fail to use server actions to mutate server data #60328

Open atomiechen opened 8 months ago

atomiechen commented 8 months ago

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:

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

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:

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.

atomiechen commented 8 months ago

Plus: the patch server component needs to execute the nop() function to make it actually import the server action.

sim391calado commented 6 months ago

Where you able to fix this?

Alegiter commented 3 weeks ago

I'm on "next": "14.2.7". Encountered same problem with data in memory. But can't successfully apply suggested workaround with Nop component :(