From 6ba0a367dad4b191630ef0fba397e2dca21d8a1e Mon Sep 17 00:00:00 2001 From: Janka Uryga Date: Mon, 27 Jan 2025 15:06:42 +0100 Subject: [PATCH 1/3] fix: don't drop queued actions when navigating --- .../src/shared/lib/router/action-queue.ts | 3 +- .../app/layout.tsx | 16 ++++++++++ .../app/page.tsx | 30 +++++++++++++++++++ .../app/server.ts | 3 ++ .../next.config.mjs | 1 + 5 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 test/e2e/app-dir/navigation-with-queued-actions/app/layout.tsx create mode 100644 test/e2e/app-dir/navigation-with-queued-actions/app/page.tsx create mode 100644 test/e2e/app-dir/navigation-with-queued-actions/app/server.ts create mode 100644 test/e2e/app-dir/navigation-with-queued-actions/next.config.mjs diff --git a/packages/next/src/shared/lib/router/action-queue.ts b/packages/next/src/shared/lib/router/action-queue.ts index 7c62b78b2d62c..2c176359634d6 100644 --- a/packages/next/src/shared/lib/router/action-queue.ts +++ b/packages/next/src/shared/lib/router/action-queue.ts @@ -151,8 +151,7 @@ function dispatchAction( // Mark the pending action as discarded (so the state is never applied) and start the navigation action immediately. actionQueue.pending.discarded = true - // Mark this action as the last in the queue - actionQueue.last = newAction + newAction.next = actionQueue.pending.next // if the pending action was a server action, mark the queue as needing a refresh once events are processed if (actionQueue.pending.payload.type === ACTION_SERVER_ACTION) { diff --git a/test/e2e/app-dir/navigation-with-queued-actions/app/layout.tsx b/test/e2e/app-dir/navigation-with-queued-actions/app/layout.tsx new file mode 100644 index 0000000000000..a14e64fcd5e33 --- /dev/null +++ b/test/e2e/app-dir/navigation-with-queued-actions/app/layout.tsx @@ -0,0 +1,16 @@ +export const metadata = { + title: 'Next.js', + description: 'Generated by Next.js', +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/test/e2e/app-dir/navigation-with-queued-actions/app/page.tsx b/test/e2e/app-dir/navigation-with-queued-actions/app/page.tsx new file mode 100644 index 0000000000000..eff55d68d2de3 --- /dev/null +++ b/test/e2e/app-dir/navigation-with-queued-actions/app/page.tsx @@ -0,0 +1,30 @@ +'use client' + +import { useRouter } from 'next/navigation' +import { useEffect, useState } from 'react' +import { myAction } from './server' + +export default function Page() { + const router = useRouter() + const [text, setText] = useState('initial') + + useEffect(() => { + let count = 1 + const myActionWrapped = async () => { + const id = count++ + console.log('myAction()', `[call number ${id}]`) + + await myAction() + console.log('-> myAction() finished', `[call number ${id}]`) + } + Promise.all([myActionWrapped(), myActionWrapped()]).then(() => + setText('actions finished') + ) + setTimeout(() => { + console.log(`router.replace('?')`) + router.replace('?') + }) + }, [router]) + + return <>{text} +} diff --git a/test/e2e/app-dir/navigation-with-queued-actions/app/server.ts b/test/e2e/app-dir/navigation-with-queued-actions/app/server.ts new file mode 100644 index 0000000000000..6f41baed884a1 --- /dev/null +++ b/test/e2e/app-dir/navigation-with-queued-actions/app/server.ts @@ -0,0 +1,3 @@ +'use server' + +export const myAction = async () => {} diff --git a/test/e2e/app-dir/navigation-with-queued-actions/next.config.mjs b/test/e2e/app-dir/navigation-with-queued-actions/next.config.mjs new file mode 100644 index 0000000000000..b1c6ea436a540 --- /dev/null +++ b/test/e2e/app-dir/navigation-with-queued-actions/next.config.mjs @@ -0,0 +1 @@ +export default {} From 59d8dc601a95855c5582f34f288c5a80fec02414 Mon Sep 17 00:00:00 2001 From: Janka Uryga Date: Mon, 27 Jan 2025 17:36:38 +0100 Subject: [PATCH 2/3] comment --- packages/next/src/shared/lib/router/action-queue.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/next/src/shared/lib/router/action-queue.ts b/packages/next/src/shared/lib/router/action-queue.ts index 2c176359634d6..e01648aa0981b 100644 --- a/packages/next/src/shared/lib/router/action-queue.ts +++ b/packages/next/src/shared/lib/router/action-queue.ts @@ -151,6 +151,8 @@ function dispatchAction( // Mark the pending action as discarded (so the state is never applied) and start the navigation action immediately. actionQueue.pending.discarded = true + // The rest of the current queue should still execute after this navigation. + // (Note that it can't contain any earlier navigations, because we always put those into `actionQueue.pending` by calling `runAction`) newAction.next = actionQueue.pending.next // if the pending action was a server action, mark the queue as needing a refresh once events are processed From b46ec1722e8bbe42cb67f08858943b2be1b4daf0 Mon Sep 17 00:00:00 2001 From: Janka Uryga Date: Mon, 27 Jan 2025 19:23:47 +0100 Subject: [PATCH 3/3] test: wip --- .../app/page.tsx | 37 +++++++++---------- .../app/server.ts | 7 +++- .../index.test.ts | 18 +++++++++ 3 files changed, 41 insertions(+), 21 deletions(-) create mode 100644 test/e2e/app-dir/navigation-with-queued-actions/index.test.ts diff --git a/test/e2e/app-dir/navigation-with-queued-actions/app/page.tsx b/test/e2e/app-dir/navigation-with-queued-actions/app/page.tsx index eff55d68d2de3..2d3c4fe1c758c 100644 --- a/test/e2e/app-dir/navigation-with-queued-actions/app/page.tsx +++ b/test/e2e/app-dir/navigation-with-queued-actions/app/page.tsx @@ -1,30 +1,27 @@ 'use client' import { useRouter } from 'next/navigation' -import { useEffect, useState } from 'react' +import { useState } from 'react' import { myAction } from './server' export default function Page() { const router = useRouter() const [text, setText] = useState('initial') - useEffect(() => { - let count = 1 - const myActionWrapped = async () => { - const id = count++ - console.log('myAction()', `[call number ${id}]`) - - await myAction() - console.log('-> myAction() finished', `[call number ${id}]`) - } - Promise.all([myActionWrapped(), myActionWrapped()]).then(() => - setText('actions finished') - ) - setTimeout(() => { - console.log(`router.replace('?')`) - router.replace('?') - }) - }, [router]) - - return <>{text} + return ( + <> + +
{text}
+ + ) } diff --git a/test/e2e/app-dir/navigation-with-queued-actions/app/server.ts b/test/e2e/app-dir/navigation-with-queued-actions/app/server.ts index 6f41baed884a1..0512ff0020a27 100644 --- a/test/e2e/app-dir/navigation-with-queued-actions/app/server.ts +++ b/test/e2e/app-dir/navigation-with-queued-actions/app/server.ts @@ -1,3 +1,8 @@ 'use server' -export const myAction = async () => {} +import { setTimeout } from 'timers/promises' + +export async function myAction(id: number) { + console.log(`myAction(${id}) :: server`) + await setTimeout(100) +} diff --git a/test/e2e/app-dir/navigation-with-queued-actions/index.test.ts b/test/e2e/app-dir/navigation-with-queued-actions/index.test.ts new file mode 100644 index 0000000000000..fd0669c1013f0 --- /dev/null +++ b/test/e2e/app-dir/navigation-with-queued-actions/index.test.ts @@ -0,0 +1,18 @@ +import { nextTestSetup } from '../../../lib/e2e-utils' +import { retry } from '../../../lib/next-test-utils' + +describe('actions', () => { + const { next } = nextTestSetup({ files: __dirname }) + it('works', async () => { + const browser = await next.browser('/') + await browser.elementByCss('button').click() + await retry( + async () => { + expect(await browser.elementById('action-state').text()).toEqual('done') + }, + undefined, + undefined, + 'wait for both actions to finish' + ) + }) +})