diff --git a/packages/next/src/client/components/app-router.tsx b/packages/next/src/client/components/app-router.tsx index f178f30f4ccf2..7eed516fb8528 100644 --- a/packages/next/src/client/components/app-router.tsx +++ b/packages/next/src/client/components/app-router.tsx @@ -132,7 +132,7 @@ function HistoryUpdater({ tree, pushRef, canonicalUrl, sync }: any) { return null } -const createEmptyCacheNode = () => ({ +export const createEmptyCacheNode = () => ({ status: CacheStates.LAZY_INITIALIZED, data: null, subTreeData: null, diff --git a/packages/next/src/shared/lib/router/action-queue.ts b/packages/next/src/shared/lib/router/action-queue.ts index 45118a705b969..df6c09d78eb22 100644 --- a/packages/next/src/shared/lib/router/action-queue.ts +++ b/packages/next/src/shared/lib/router/action-queue.ts @@ -3,10 +3,14 @@ import { type AppRouterState, type ReducerActions, type ReducerState, + ACTION_REFRESH, + ACTION_SERVER_ACTION, + ACTION_NAVIGATE, } from '../../../client/components/router-reducer/router-reducer-types' import type { ReduxDevToolsInstance } from '../../../client/components/use-reducer-with-devtools' import { reducer } from '../../../client/components/router-reducer/router-reducer' import React, { startTransition } from 'react' +import { createEmptyCacheNode } from '../../../client/components/app-router' export type DispatchStatePromise = React.Dispatch @@ -16,6 +20,7 @@ export type AppRouterActionQueue = { dispatch: (payload: ReducerActions, setState: DispatchStatePromise) => void action: (state: AppRouterState, action: ReducerActions) => ReducerState pending: ActionQueueNode | null + needsRefresh?: boolean last: ActionQueueNode | null } @@ -70,7 +75,21 @@ async function runAction({ function handleResult(nextState: AppRouterState) { // if we discarded this action, the state should also be discarded - if (action.discarded) return + if (action.discarded) { + if (actionQueue.needsRefresh) { + actionQueue.needsRefresh = false + actionQueue.dispatch( + { + type: ACTION_REFRESH, + cache: createEmptyCacheNode(), + mutable: {}, + origin: window.location.origin, + }, + setState + ) + } + return + } actionQueue.state = nextState @@ -121,11 +140,15 @@ function dispatchAction( setState(deferredPromise) }) - if (payload.type === 'navigate' && actionQueue.pending !== null) { + if (payload.type === ACTION_NAVIGATE && actionQueue.pending !== null) { // Navigations take priority over any pending actions. // Mark the pending action as discarded (so the state is never applied) and start the navigation action immediately. actionQueue.pending.discarded = true + // if the pending action was a server action, mark the queue as needing a refresh once events are processed + actionQueue.needsRefresh = + actionQueue.pending.payload.type === ACTION_SERVER_ACTION + runAction({ actionQueue, action: newAction, diff --git a/test/e2e/app-dir/actions/app-action.test.ts b/test/e2e/app-dir/actions/app-action.test.ts index 08e3705074d89..2ca61d6c9c539 100644 --- a/test/e2e/app-dir/actions/app-action.test.ts +++ b/test/e2e/app-dir/actions/app-action.test.ts @@ -288,6 +288,27 @@ createNextDescribe( await check(() => browser.url(), `${next.url}/client`, true, 2) }) + it('should trigger a refresh for a server action that gets discarded due to a navigation', async () => { + let browser = await next.browser('/client') + const initialRandomNumber = await browser + .elementByCss('#random-number') + .text() + + await browser.elementByCss('#slow-inc').click() + + // navigate to server + await browser.elementByCss('#navigate-server').click() + + // wait for the action to be completed + await check(async () => { + const newRandomNumber = await browser + .elementByCss('#random-number') + .text() + + return newRandomNumber === initialRandomNumber ? 'fail' : 'success' + }, 'success') + }) + it('should support next/dynamic with ssr: false', async () => { const browser = await next.browser('/dynamic-csr')