Skip to content

Commit fa397f5

Browse files
committed
[Cache Components] Disable static indicator
When `experimental.cacheComponents` is enabled, the static indicator in the Next.js DevTools is now disabled. With Cache Components enabled, the binary static/dynamic state is no longer accurate since routes are typically partially static and dynamic (unless fully opted into dynamic rendering via a Suspense boundary above the body, which is an edge case). In the future, we will likely provide better tools that will allow users to understand which parts of a page are prerendered and which parts are dynamic. closes NAR-428
1 parent 809f37e commit fa397f5

File tree

23 files changed

+460
-194
lines changed

23 files changed

+460
-194
lines changed

packages/next/src/client/app-index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ export function hydrate(
260260
const { createWebSocket } =
261261
require('./dev/hot-reloader/app/web-socket') as typeof import('./dev/hot-reloader/app/web-socket')
262262

263-
staticIndicatorState = { pathname: null, appIsrManifest: {} }
263+
staticIndicatorState = { pathname: null, appIsrManifest: null }
264264
webSocket = createWebSocket(assetPrefix, staticIndicatorState)
265265
}
266266

packages/next/src/client/dev/hot-reloader/app/hot-reloader-app.tsx

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import { getOrCreateDebugChannelReadableWriterPair } from '../../debug-channel'
4141

4242
export interface StaticIndicatorState {
4343
pathname: string | null
44-
appIsrManifest: Record<string, true>
44+
appIsrManifest: Record<string, boolean> | null
4545
}
4646

4747
let mostRecentCompilationHash: any = null
@@ -261,18 +261,18 @@ export function processMessage(
261261
if (process.env.__NEXT_DEV_INDICATOR) {
262262
staticIndicatorState.appIsrManifest = message.data
263263

264-
// handle initial status on receiving manifest
265-
// navigation is handled in useEffect for pathname changes
266-
// as we'll receive the updated manifest before usePathname
267-
// triggers for new value
268-
if (
269-
staticIndicatorState.pathname &&
270-
staticIndicatorState.pathname in message.data
271-
) {
272-
dispatcher.onStaticIndicator(true)
273-
} else {
274-
dispatcher.onStaticIndicator(false)
275-
}
264+
// Handle the initial static indicator status on receiving the ISR
265+
// manifest. Navigation is handled in an effect inside HotReload for
266+
// pathname changes as we'll receive the updated manifest before
267+
// usePathname triggers for a new value.
268+
269+
const isStatic = staticIndicatorState.pathname
270+
? message.data[staticIndicatorState.pathname]
271+
: undefined
272+
273+
dispatcher.onStaticIndicator(
274+
isStatic === undefined ? 'pending' : isStatic ? 'static' : 'dynamic'
275+
)
276276
}
277277
break
278278
}
@@ -542,10 +542,14 @@ export default function HotReload({
542542

543543
staticIndicatorState.pathname = pathname
544544

545-
if (pathname && pathname in staticIndicatorState.appIsrManifest) {
546-
dispatcher.onStaticIndicator(true)
547-
} else {
548-
dispatcher.onStaticIndicator(false)
545+
if (staticIndicatorState.appIsrManifest) {
546+
const isStatic = pathname
547+
? staticIndicatorState.appIsrManifest[pathname]
548+
: undefined
549+
550+
dispatcher.onStaticIndicator(
551+
isStatic === undefined ? 'pending' : isStatic ? 'static' : 'dynamic'
552+
)
549553
}
550554
}, [pathname, staticIndicatorState])
551555
}

packages/next/src/client/dev/hot-reloader/pages/hot-reloader-pages.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,10 +265,10 @@ export function handleStaticIndicator() {
265265
appComponent?.getInitialProps !== appComponent?.origGetInitialProps
266266

267267
const isPageStatic =
268-
window.location.pathname in isrManifest ||
268+
isrManifest[window.location.pathname] ||
269269
(!isDynamicPage && !hasAppGetInitialProps)
270270

271-
dispatcher.onStaticIndicator(isPageStatic)
271+
dispatcher.onStaticIndicator(isPageStatic ? 'static' : 'dynamic')
272272
}
273273
}
274274

packages/next/src/next-devtools/dev-overlay.browser.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export interface Dispatcher {
5555
onDebugInfo(debugInfo: DebugInfo): void
5656
onBeforeRefresh(): void
5757
onRefresh(): void
58-
onStaticIndicator(status: boolean): void
58+
onStaticIndicator(status: 'pending' | 'static' | 'dynamic' | 'disabled'): void
5959
onDevIndicator(devIndicator: DevIndicatorServerState): void
6060
onDevToolsConfig(config: DevToolsConfig): void
6161
onUnhandledError(reason: Error): void
@@ -147,9 +147,14 @@ export const dispatcher: Dispatcher = {
147147
dispatch({ type: ACTION_VERSION_INFO, versionInfo })
148148
}
149149
),
150-
onStaticIndicator: createQueuable((dispatch: Dispatch, status: boolean) => {
151-
dispatch({ type: ACTION_STATIC_INDICATOR, staticIndicator: status })
152-
}),
150+
onStaticIndicator: createQueuable(
151+
(
152+
dispatch: Dispatch,
153+
status: 'pending' | 'static' | 'dynamic' | 'disabled'
154+
) => {
155+
dispatch({ type: ACTION_STATIC_INDICATOR, staticIndicator: status })
156+
}
157+
),
153158
onDebugInfo: createQueuable((dispatch: Dispatch, debugInfo: DebugInfo) => {
154159
dispatch({ type: ACTION_DEBUG_INFO, debugInfo })
155160
}),
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
export function LoadingIcon() {
2+
return (
3+
<svg
4+
width="20px"
5+
height="20px"
6+
viewBox="0 0 20 20"
7+
fill="none"
8+
xmlns="http://www.w3.org/2000/svg"
9+
>
10+
<circle
11+
cx="10"
12+
cy="10"
13+
r="7"
14+
stroke="currentColor"
15+
strokeWidth="2"
16+
strokeLinecap="round"
17+
strokeDasharray="32 12"
18+
opacity="0.8"
19+
>
20+
<animateTransform
21+
attributeName="transform"
22+
type="rotate"
23+
from="0 10 10"
24+
to="360 10 10"
25+
dur="1s"
26+
repeatCount="indefinite"
27+
/>
28+
</circle>
29+
</svg>
30+
)
31+
}

packages/next/src/next-devtools/dev-overlay/menu/panel-router.tsx

Lines changed: 53 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
ACTION_ERROR_OVERLAY_OPEN,
2525
} from '../shared'
2626
import GearIcon from '../icons/gear-icon'
27+
import { LoadingIcon } from '../icons/loading-icon'
2728
import { UserPreferencesBody } from '../components/errors/dev-tools-indicator/dev-tools-info/user-preferences'
2829
import { useShortcuts } from '../hooks/use-shortcuts'
2930
import { useUpdateAllPanelPositions } from '../components/devtools-indicator/devtools-indicator'
@@ -60,17 +61,26 @@ const MenuPanel = () => {
6061
}
6162
},
6263
},
63-
{
64-
title: `Current route is ${state.staticIndicator ? 'static' : 'dynamic'}.`,
65-
label: 'Route',
66-
value: state.staticIndicator ? 'Static' : 'Dynamic',
67-
onClick: () => setPanel('route-type'),
68-
attributes: {
69-
'data-nextjs-route-type': state.staticIndicator
70-
? 'static'
71-
: 'dynamic',
72-
},
73-
},
64+
state.staticIndicator === 'disabled'
65+
? undefined
66+
: state.staticIndicator === 'pending'
67+
? {
68+
title: 'Loading...',
69+
label: 'Route',
70+
value: <LoadingIcon />,
71+
}
72+
: {
73+
title: `Current route is ${state.staticIndicator ? 'static' : 'dynamic'}.`,
74+
label: 'Route',
75+
value:
76+
state.staticIndicator === 'static' ? 'Static' : 'Dynamic',
77+
onClick: () => setPanel('route-type'),
78+
attributes: {
79+
'data-nextjs-route-type': state.staticIndicator
80+
? 'static'
81+
: 'dynamic',
82+
},
83+
},
7484
!!process.env.TURBOPACK
7585
? {
7686
title: 'Turbopack is enabled.',
@@ -166,39 +176,39 @@ export const PanelRouter = () => {
166176
</DynamicPanel>
167177
</PanelRoute>
168178

169-
<PanelRoute name="route-type">
170-
<DynamicPanel
171-
key={state.staticIndicator ? 'static' : 'dynamic'}
172-
sharePanelSizeGlobally={false}
173-
sizeConfig={{
174-
kind: 'fixed',
175-
height: state.staticIndicator
176-
? 300 / state.scale
177-
: 325 / state.scale,
178-
width: 400 / state.scale,
179-
}}
180-
closeOnClickOutside
181-
header={
182-
<DevToolsHeader
183-
title={`${state.staticIndicator ? 'Static' : 'Dynamic'} Route`}
184-
/>
185-
}
186-
>
187-
<div className="panel-content">
188-
<RouteInfoBody
189-
routerType={state.routerType}
190-
isStaticRoute={state.staticIndicator}
191-
/>
192-
<InfoFooter
193-
href={
194-
learnMoreLink[state.routerType][
195-
state.staticIndicator ? 'static' : 'dynamic'
196-
]
179+
{state.staticIndicator !== 'disabled' &&
180+
state.staticIndicator !== 'pending' && (
181+
<PanelRoute name="route-type">
182+
<DynamicPanel
183+
key={state.staticIndicator}
184+
sharePanelSizeGlobally={false}
185+
sizeConfig={{
186+
kind: 'fixed',
187+
height:
188+
state.staticIndicator === 'static'
189+
? 300 / state.scale
190+
: 325 / state.scale,
191+
width: 400 / state.scale,
192+
}}
193+
closeOnClickOutside
194+
header={
195+
<DevToolsHeader
196+
title={`${state.staticIndicator ? 'Static' : 'Dynamic'} Route`}
197+
/>
197198
}
198-
/>
199-
</div>
200-
</DynamicPanel>
201-
</PanelRoute>
199+
>
200+
<div className="panel-content">
201+
<RouteInfoBody
202+
routerType={state.routerType}
203+
isStaticRoute={state.staticIndicator === 'static'}
204+
/>
205+
<InfoFooter
206+
href={learnMoreLink[state.routerType][state.staticIndicator]}
207+
/>
208+
</div>
209+
</DynamicPanel>
210+
</PanelRoute>
211+
)}
202212

203213
{isAppRouter && (
204214
<PanelRoute name="segment-explorer">

packages/next/src/next-devtools/dev-overlay/shared.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export interface OverlayState {
4949
readonly notFound: boolean
5050
readonly buildingIndicator: boolean
5151
readonly renderingIndicator: boolean
52-
readonly staticIndicator: boolean
52+
readonly staticIndicator: 'pending' | 'static' | 'dynamic' | 'disabled'
5353
readonly showIndicator: boolean
5454
readonly disableDevIndicator: boolean
5555
readonly debugInfo: DebugInfo
@@ -112,7 +112,7 @@ export const ACTION_DEVTOOL_UPDATE_ROUTE_STATE =
112112

113113
interface StaticIndicatorAction {
114114
type: typeof ACTION_STATIC_INDICATOR
115-
staticIndicator: boolean
115+
staticIndicator: 'pending' | 'static' | 'dynamic' | 'disabled'
116116
}
117117

118118
interface BuildOkAction {
@@ -263,7 +263,7 @@ export const INITIAL_OVERLAY_STATE: Omit<
263263
errors: [],
264264
notFound: false,
265265
renderingIndicator: false,
266-
staticIndicator: false,
266+
staticIndicator: 'disabled',
267267
/*
268268
This is set to `true` when we can reliably know
269269
whether the indicator is in disabled state or not.

packages/next/src/server/app-render/app-render.tsx

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1502,6 +1502,7 @@ async function renderToHTMLOrFlightImpl(
15021502
serverActions,
15031503
assetPrefix = '',
15041504
enableTainting,
1505+
experimental,
15051506
} = renderOpts
15061507

15071508
// We need to expose the bundled `require` API globally for
@@ -1566,10 +1567,18 @@ async function renderToHTMLOrFlightImpl(
15661567
globalThis.__next_chunk_load__ = __next_chunk_load__
15671568
}
15681569

1569-
if (process.env.NODE_ENV === 'development') {
1570-
// reset isr status at start of request
1570+
if (
1571+
process.env.NODE_ENV === 'development' &&
1572+
renderOpts.setIsrStatus &&
1573+
!experimental.cacheComponents
1574+
) {
1575+
// Reset the ISR status at start of request.
15711576
const { pathname } = new URL(req.url || '/', 'http://n')
1572-
renderOpts.setIsrStatus?.(pathname, false)
1577+
renderOpts.setIsrStatus(
1578+
pathname,
1579+
// Only pages using the Node runtime can use ISR, Edge is always dynamic.
1580+
process.env.NEXT_RUNTIME === 'edge' ? false : undefined
1581+
)
15731582
}
15741583

15751584
if (
@@ -1844,19 +1853,19 @@ async function renderToHTMLOrFlightImpl(
18441853
if (
18451854
process.env.NODE_ENV === 'development' &&
18461855
renderOpts.setIsrStatus &&
1856+
!experimental.cacheComponents &&
1857+
// Only pages using the Node runtime can use ISR, so we only need to
1858+
// update the status for those.
18471859
// The type check here ensures that `req` is correctly typed, and the
18481860
// environment variable check provides dead code elimination.
18491861
process.env.NEXT_RUNTIME !== 'edge' &&
1850-
isNodeNextRequest(req) &&
1851-
!isDevWarmupRequest
1862+
isNodeNextRequest(req)
18521863
) {
18531864
const setIsrStatus = renderOpts.setIsrStatus
18541865
req.originalRequest.on('end', () => {
1855-
if (!requestStore.usedDynamic && !workStore.forceDynamic) {
1856-
// only node can be ISR so we only need to update the status here
1857-
const { pathname } = new URL(req.url || '/', 'http://n')
1858-
setIsrStatus(pathname, true)
1859-
}
1866+
const { pathname } = new URL(req.url || '/', 'http://n')
1867+
const isStatic = !requestStore.usedDynamic && !workStore.forceDynamic
1868+
setIsrStatus(pathname, isStatic)
18601869
})
18611870
}
18621871

packages/next/src/server/app-render/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export interface RenderOptsPartial {
9393
}
9494
isOnDemandRevalidate?: boolean
9595
isPossibleServerAction?: boolean
96-
setIsrStatus?: (key: string, value: boolean) => void
96+
setIsrStatus?: (key: string, value: boolean | undefined) => void
9797
setReactDebugChannel?: (
9898
debugChannel: { readable: ReadableStream<Uint8Array> },
9999
htmlRequestId: string,

0 commit comments

Comments
 (0)