Skip to content

Bug: Suspense wrapped component does not update correctly #36120

@MaxencePaulin

Description

@MaxencePaulin

When a server component is wrapped with Suspense tag, revalidatePath does not work correctly.
Only when npm run build && npm run start

React version: 19.2.4
Next.js version: 16.2.0

Steps To Reproduce

  1. Wrap a Server component (list of item) with Suspense tag
  2. npm run build && npm run start
  3. Delete an item with the UI button (will delete and revalidatePath)
  4. See not UI update after the request / response

Link to code example:

https://github.com/vercel/next-learn/blob/main/dashboard/final-example/app/dashboard/invoices/page.tsx

I tried to reproduce the bug on CodeSandbox, but i think the cache / revalidatePath does not work as my own machine. Sorry.

The code is maybe not the best, i did the Acme course on NextJS 16.

If there is some better pratice, i will be glad to learn them. (except the interaction with Data, i will do an API for that i think. Server action will retrieve some data from an external API)

// app/dashboard/invoices/page.tsx
import Pagination from '@/app/ui/invoices/pagination';
import Search from '@/app/ui/search';
import Table from '@/app/ui/invoices/table';
import { CreateInvoice } from '@/app/ui/invoices/buttons';
import { lusitana } from '@/app/ui/fonts';
import { InvoicesTableSkeleton } from '@/app/ui/skeletons';
import { Suspense } from 'react';
import { fetchInvoicesPages } from '@/app/lib/data';

export default async function Page(props: { searchParams?: Promise<{ query?: string; page?: string }> }) {
    const searchParams = await props.searchParams;
    const query = searchParams?.query || '';
    const pageParameter = Number.parseInt(searchParams?.page || '1', 10);
    const currentPage = Number.isNaN(pageParameter) || pageParameter < 1 ? 1 : pageParameter;
    const totalPages = await fetchInvoicesPages(query);

    return (
        <div className="w-full">
            <div className="flex w-full items-center justify-between">
                <h1 className={`${lusitana.className} text-2xl`}>Invoices</h1>
            </div>
            <div className="mt-4 flex items-center justify-between gap-2 md:mt-8">
                <Search placeholder="Search invoices..." />
                <CreateInvoice />
            </div>
            <Suspense key={query + currentPage} fallback={<InvoicesTableSkeleton />}>
                <Table query={query} currentPage={currentPage} />
            </Suspense>
            <div className="mt-5 flex w-full justify-center">
                <Pagination totalPages={totalPages} />
            </div>
        </div>
    );
}
// app/ui/invoices/table.tsx
import Image from 'next/image';
import { UpdateInvoice, DeleteInvoice } from '@/app/ui/invoices/buttons';
import InvoiceStatus from '@/app/ui/invoices/status';
import { formatDateToLocal, formatCurrency } from '@/app/lib/utils';
import { fetchFilteredInvoices } from '@/app/lib/data';

export default async function InvoicesTable({ query, currentPage }: { query: string; currentPage: number }) {
    const invoices = await fetchFilteredInvoices(query, currentPage);

    return (
        <div className="mt-6 flow-root">
            <div className="inline-block min-w-full align-middle">
                <div className="rounded-lg bg-gray-50 p-2 md:pt-0">
                    ...
                    <table className="hidden min-w-full text-gray-900 md:table">
                        <thead className="rounded-lg text-left text-sm font-normal">
                            <tr>
                                <th scope="col" className="px-4 py-5 font-medium sm:pl-6">
                                    Customer
                                </th>
                                <th scope="col" className="px-3 py-5 font-medium">
                                    Email
                                </th>
                                <th scope="col" className="px-3 py-5 font-medium">
                                    Amount
                                </th>
                                <th scope="col" className="px-3 py-5 font-medium">
                                    Date
                                </th>
                                <th scope="col" className="px-3 py-5 font-medium">
                                    Status
                                </th>
                                <th scope="col" className="relative py-3 pl-6 pr-3">
                                    <span className="sr-only">Edit</span>
                                </th>
                            </tr>
                        </thead>
                        <tbody className="bg-white">
                            {invoices?.map((invoice) => (
                                <tr
                                    key={invoice.id}
                                    className="w-full border-b py-3 text-sm last-of-type:border-none [&:first-child>td:first-child]:rounded-tl-lg [&:first-child>td:last-child]:rounded-tr-lg [&:last-child>td:first-child]:rounded-bl-lg [&:last-child>td:last-child]:rounded-br-lg"
                                >
                                    <td className="whitespace-nowrap py-3 pl-6 pr-3">
                                        <div className="flex items-center gap-3">
                                            <Image
                                                src={invoice.image_url}
                                                className="rounded-full"
                                                width={28}
                                                height={28}
                                                alt={`${invoice.name}'s profile picture`}
                                            />
                                            <p>{invoice.name}</p>
                                        </div>
                                    </td>
                                    <td className="whitespace-nowrap px-3 py-3">{invoice.email}</td>
                                    <td className="whitespace-nowrap px-3 py-3">{formatCurrency(invoice.amount)}</td>
                                    <td className="whitespace-nowrap px-3 py-3">{formatDateToLocal(invoice.date)}</td>
                                    <td className="whitespace-nowrap px-3 py-3">
                                        <InvoiceStatus status={invoice.status} />
                                    </td>
                                    <td className="whitespace-nowrap py-3 pl-6 pr-3">
                                        <div className="flex justify-end gap-3">
                                            <UpdateInvoice id={invoice.id} />
                                            <DeleteInvoice id={invoice.id} />
                                        </div>
                                    </td>
                                </tr>
                            ))}
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    );
}
// app/ui/invoices/buttons.tsx
...
export function DeleteInvoice({ id }: { id: string }) {
    const deleteInvoiceWithId = deleteInvoice.bind(null, id);

    return (
        <form action={deleteInvoiceWithId}>
            <button type="submit" className="rounded-md border p-2 hover:bg-gray-100">
                <span className="sr-only">Delete</span>
                <TrashIcon className="w-5" />
            </button>
        </form>
    );
}
// app/lib/action.ts
...
export async function deleteInvoice(id: string) {
    try {
        await sql`DELETE FROM invoices WHERE id = ${id}`;
    } catch (error) {
        console.error(error);
        // Add toast for error ?
        return;
    }

    revalidatePath('/dashboard/invoices');
}

The current behavior

When deleting an item, the UI is not refreshed.

The expected behavior

With revalidatePath, we should have an UI up to date after deleting an element.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Status: UnconfirmedA potential issue that we haven't yet confirmed as a bug

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions