Skip to content

Commit 3fc6846

Browse files
committed
Refresh on route tree mismatch
During a dynamic navigation, the server may respond with a different route tree than the one we expected. This typically happens due to a conditional server-side routing change. This shouldn't happen under normal circumstances, but in some cases it's unavoidable. Such as a user logging in/out from a different client. When it does happen, Next.js must recover to a consistent state. This may involve a brief flash of the incorrect route before the mismatch is detected. The recovery mechanism is simple enough: we refresh the page, using whatever tree the server responds with (bypassing any older version we may have cached locally). Also, because a route mismatch implies some sort of server-side mutation, we also evict all the dynamic data in the client's cache. Similar to what we do when a cookie is mutated by a Server Action, or in response to a refresh() call. This replaces the need for the lazy data fetch that happens in LayoutRouter, via the server-patch reducer. I've left the existing server-patch logic in place until I've finished migrating the reducers over to the new implementation. In the meantime, I updated LayoutRouter to disable the lazy data fetching branch for any navigation produced by a reducer that has already been ported.
1 parent 6cffd64 commit 3fc6846

File tree

19 files changed

+709
-179
lines changed

19 files changed

+709
-179
lines changed

packages/next/src/client/components/app-router-instance.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,7 @@ function runRemainingActions(
7979
// after the navigation has already finished and the queue is empty
8080
if (actionQueue.needsRefresh) {
8181
actionQueue.needsRefresh = false
82-
actionQueue.dispatch(
83-
{
84-
type: ACTION_REFRESH,
85-
origin: window.location.origin,
86-
},
87-
setState
88-
)
82+
actionQueue.dispatch({ type: ACTION_REFRESH }, setState)
8983
}
9084
}
9185
}
@@ -383,7 +377,6 @@ export const publicAppRouterInstance: AppRouterInstance = {
383377
startTransition(() => {
384378
dispatchAppRouterAction({
385379
type: ACTION_REFRESH,
386-
origin: window.location.origin,
387380
})
388381
})
389382
},

packages/next/src/client/components/layout-router.tsx

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import {
5050
} from '../../shared/lib/hooks-client-context.shared-runtime'
5151
import { getParamValueFromCacheKey } from '../route-params'
5252
import type { Params } from '../../server/request/params'
53+
import { isDeferredRsc } from './router-reducer/ppr-navigations'
5354

5455
/**
5556
* Add refetch marker to router state at the point of the current layout segment.
@@ -375,11 +376,27 @@ function InnerLayoutRouter({
375376
// special case `null` to represent that this segment's data is missing. If
376377
// it's a promise, we need to unwrap it so we can determine whether or not the
377378
// data is missing.
378-
const resolvedRsc: React.ReactNode =
379-
typeof rsc === 'object' && rsc !== null && typeof rsc.then === 'function'
380-
? use(rsc)
381-
: rsc
379+
let resolvedRsc: React.ReactNode
380+
if (isDeferredRsc(rsc)) {
381+
const unwrappedRsc = use(rsc)
382+
if (unwrappedRsc === null) {
383+
// If the promise was resolved to `null`, it means the data for this
384+
// segment was not returned by the server. Suspend indefinitely. When this
385+
// happens, the router is responsible for triggering a new state update to
386+
// un-suspend this segment.
387+
use(unresolvedThenable) as never
388+
}
389+
resolvedRsc = unwrappedRsc
390+
} else {
391+
// This is not a deferred RSC promise. Don't need to unwrap it.
392+
resolvedRsc = rsc
393+
}
382394

395+
// TODO: At this point, the only reason `resolvedRsc` would be null is if the
396+
// data for this segment was fetched by a reducer that hasn't been migrated
397+
// yet to the Segment Cache implementation. It shouldn't happen for regular
398+
// navigations. Once we convert the remaining reducers, we can delete the
399+
// lazy fetching block below.
383400
if (!resolvedRsc) {
384401
// The data for this segment is not available, and there's no pending
385402
// navigation that will be able to fulfill it. We need to fetch more from
@@ -418,6 +435,7 @@ function InnerLayoutRouter({
418435
previousTree: fullTree,
419436
serverResponse,
420437
navigatedAt,
438+
retry: null,
421439
})
422440
})
423441

0 commit comments

Comments
 (0)