Skip to content

Commit dc52e7e

Browse files
authored
fix: don't drop queued actions when navigating (#75362)
If we run two or more actions, the first starts executing immediately, and the rest gets queued. The bug here was that if we navigated right after, the navigation would make us drop the rest of the queue, which would cause all the queued action calls to never resolve.
1 parent 910b07b commit dc52e7e

File tree

6 files changed

+73
-2
lines changed

6 files changed

+73
-2
lines changed

packages/next/src/shared/lib/router/action-queue.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,9 @@ function dispatchAction(
151151
// Mark the pending action as discarded (so the state is never applied) and start the navigation action immediately.
152152
actionQueue.pending.discarded = true
153153

154-
// Mark this action as the last in the queue
155-
actionQueue.last = newAction
154+
// The rest of the current queue should still execute after this navigation.
155+
// (Note that it can't contain any earlier navigations, because we always put those into `actionQueue.pending` by calling `runAction`)
156+
newAction.next = actionQueue.pending.next
156157

157158
// if the pending action was a server action, mark the queue as needing a refresh once events are processed
158159
if (actionQueue.pending.payload.type === ACTION_SERVER_ACTION) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export const metadata = {
2+
title: 'Next.js',
3+
description: 'Generated by Next.js',
4+
}
5+
6+
export default function RootLayout({
7+
children,
8+
}: {
9+
children: React.ReactNode
10+
}) {
11+
return (
12+
<html lang="en">
13+
<body>{children}</body>
14+
</html>
15+
)
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
'use client'
2+
3+
import { useRouter } from 'next/navigation'
4+
import { useState } from 'react'
5+
import { myAction } from './server'
6+
7+
export default function Page() {
8+
const router = useRouter()
9+
const [text, setText] = useState('initial')
10+
11+
return (
12+
<>
13+
<button
14+
type="button"
15+
onClick={() => {
16+
Promise.all([myAction(0), myAction(1)]).then(() => setText('done'))
17+
setTimeout(() => {
18+
router.replace('?')
19+
})
20+
}}
21+
>
22+
run actions
23+
</button>
24+
<div id="action-state">{text}</div>
25+
</>
26+
)
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
'use server'
2+
3+
import { setTimeout } from 'timers/promises'
4+
5+
export async function myAction(id: number) {
6+
console.log(`myAction(${id}) :: server`)
7+
await setTimeout(100)
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { nextTestSetup } from '../../../lib/e2e-utils'
2+
import { retry } from '../../../lib/next-test-utils'
3+
4+
describe('actions', () => {
5+
const { next } = nextTestSetup({ files: __dirname })
6+
it('works', async () => {
7+
const browser = await next.browser('/')
8+
await browser.elementByCss('button').click()
9+
await retry(
10+
async () => {
11+
expect(await browser.elementById('action-state').text()).toEqual('done')
12+
},
13+
undefined,
14+
undefined,
15+
'wait for both actions to finish'
16+
)
17+
})
18+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default {}

0 commit comments

Comments
 (0)