Skip to content

Commit c081da5

Browse files
committed
Extend support of Pages router to React 18
1 parent 24ddd5c commit c081da5

File tree

20 files changed

+343
-131
lines changed

20 files changed

+343
-131
lines changed

.eslintrc.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@
4545
"jest/no-conditional-expect": "off",
4646
"jest/valid-title": "off",
4747
"jest/no-interpolation-in-snapshots": "off",
48-
"jest/no-export": "off"
48+
"jest/no-export": "off",
49+
"jest/no-standalone-expect": [
50+
"error",
51+
{ "additionalTestBlockFunctions": ["gateReact18"] }
52+
]
4953
}
5054
},
5155
{ "files": ["**/__tests__/**"], "env": { "jest": true } },

packages/next/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@
108108
"@opentelemetry/api": "^1.1.0",
109109
"@playwright/test": "^1.41.2",
110110
"babel-plugin-react-compiler": "*",
111-
"react": "19.0.0-rc-a99d8e8d-20240916",
112-
"react-dom": "19.0.0-rc-a99d8e8d-20240916",
111+
"react": "^18.2.0 || 19.0.0-rc-a99d8e8d-20240916",
112+
"react-dom": "^18.2.0 || 19.0.0-rc-a99d8e8d-20240916",
113113
"sass": "^1.3.0"
114114
},
115115
"peerDependenciesMeta": {

packages/next/src/build/webpack-config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,8 @@ const NEXT_PROJECT_ROOT_DIST_CLIENT = path.join(
107107
'client'
108108
)
109109

110-
if (parseInt(React.version) < 19) {
111-
throw new Error('Next.js requires react >= 19.0.0 to be installed.')
110+
if (parseInt(React.version) < 18) {
111+
throw new Error('Next.js requires react >= 18.2.0 to be installed.')
112112
}
113113

114114
export const babelIncludeRegexes: RegExp[] = [

packages/next/src/client/legacy/image.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import React, {
99
useState,
1010
type JSX,
1111
} from 'react'
12+
import * as ReactDOM from 'react-dom'
13+
import Head from '../../shared/lib/head'
1214
import {
1315
imageConfigDefault,
1416
VALID_LOADERS,
@@ -26,6 +28,8 @@ function normalizeSrc(src: string): string {
2628
return src[0] === '/' ? src.slice(1) : src
2729
}
2830

31+
const supportsFloat = typeof ReactDOM.preload === 'function'
32+
2933
const configEnv = process.env.__NEXT_IMAGE_OPTS as any as ImageConfigComplete
3034
const loadedImageURLs = new Set<string>()
3135
const allImgs = new Map<
@@ -978,6 +982,20 @@ export default function Image({
978982
}
979983
}
980984

985+
const linkProps:
986+
| React.DetailedHTMLProps<
987+
React.LinkHTMLAttributes<HTMLLinkElement>,
988+
HTMLLinkElement
989+
>
990+
| undefined = supportsFloat
991+
? undefined
992+
: {
993+
imageSrcSet: imgAttributes.srcSet,
994+
imageSizes: imgAttributes.sizes,
995+
crossOrigin: rest.crossOrigin,
996+
referrerPolicy: rest.referrerPolicy,
997+
}
998+
981999
const useLayoutEffect =
9821000
typeof window === 'undefined' ? React.useEffect : React.useLayoutEffect
9831001
const onLoadingCompleteRef = useRef(onLoadingComplete)
@@ -1044,6 +1062,27 @@ export default function Image({
10441062
) : null}
10451063
<ImageElement {...imgElementArgs} />
10461064
</span>
1065+
{!supportsFloat && priority ? (
1066+
// Note how we omit the `href` attribute, as it would only be relevant
1067+
// for browsers that do not support `imagesrcset`, and in those cases
1068+
// it would likely cause the incorrect image to be preloaded.
1069+
//
1070+
// https://html.spec.whatwg.org/multipage/semantics.html#attr-link-imagesrcset
1071+
<Head>
1072+
<link
1073+
key={
1074+
'__nimg-' +
1075+
imgAttributes.src +
1076+
imgAttributes.srcSet +
1077+
imgAttributes.sizes
1078+
}
1079+
rel="preload"
1080+
as="image"
1081+
href={imgAttributes.srcSet ? undefined : imgAttributes.src}
1082+
{...linkProps}
1083+
/>
1084+
</Head>
1085+
) : null}
10471086
</>
10481087
)
10491088
}

packages/next/src/client/use-merged-ref.ts

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,34 @@
1-
import { useMemo, type Ref } from 'react'
1+
import { useMemo, useRef, type Ref } from 'react'
22

3+
// This is a compatibility hook to support React 18 and 19 refs.
4+
// In 19, a cleanup function from refs may be returned.
5+
// In 18, returning a cleanup function creates a warning.
6+
// Since we take userspace refs, we don't know ahead of time if a cleanup function will be returned.
7+
// This implements cleanup functions with the old behavior in 18.
8+
// We know refs are always called alternating with `null` and then `T`.
9+
// So a call with `null` means we need to call the previous cleanup functions.
310
export function useMergedRef<TElement>(
411
refA: Ref<TElement>,
512
refB: Ref<TElement>
613
): Ref<TElement> {
7-
return useMemo(() => mergeRefs(refA, refB), [refA, refB])
8-
}
14+
const cleanupA = useRef<() => void>(() => {})
15+
const cleanupB = useRef<() => void>(() => {})
916

10-
export function mergeRefs<TElement>(
11-
refA: Ref<TElement>,
12-
refB: Ref<TElement>
13-
): Ref<TElement> {
14-
if (!refA || !refB) {
15-
return refA || refB
16-
}
17-
18-
return (current: TElement) => {
19-
const cleanupA = applyRef(refA, current)
20-
const cleanupB = applyRef(refB, current)
17+
return useMemo(() => {
18+
if (!refA || !refB) {
19+
return refA || refB
20+
}
2121

22-
return () => {
23-
cleanupA()
24-
cleanupB()
22+
return (current: TElement | null): void => {
23+
if (current === null) {
24+
cleanupA.current()
25+
cleanupB.current()
26+
} else {
27+
cleanupA.current = applyRef(refA, current)
28+
cleanupB.current = applyRef(refB, current)
29+
}
2530
}
26-
}
31+
}, [refA, refB])
2732
}
2833

2934
function applyRef<TElement>(

packages/next/src/server/render.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import type { Revalidate, SwrDelta } from './lib/revalidate'
4040
import type { COMPILER_NAMES } from '../shared/lib/constants'
4141

4242
import React, { type JSX } from 'react'
43-
import ReactDOMServerEdge from 'react-dom/server.edge'
43+
import ReactDOMServerBrowser from 'react-dom/server.browser'
4444
import { StyleRegistry, createStyleRegistry } from 'styled-jsx'
4545
import {
4646
GSP_NO_RETURNED_VALUE,
@@ -127,7 +127,8 @@ function noRouter() {
127127
}
128128

129129
async function renderToString(element: React.ReactElement) {
130-
const renderStream = await ReactDOMServerEdge.renderToReadableStream(element)
130+
const renderStream =
131+
await ReactDOMServerBrowser.renderToReadableStream(element)
131132
await renderStream.allReady
132133
return streamToString(renderStream)
133134
}
@@ -1326,7 +1327,7 @@ export async function renderToHTMLImpl(
13261327
) => {
13271328
const content = renderContent(EnhancedApp, EnhancedComponent)
13281329
return await renderToInitialFizzStream({
1329-
ReactDOMServer: ReactDOMServerEdge,
1330+
ReactDOMServer: ReactDOMServerBrowser,
13301331
element: content,
13311332
})
13321333
}

packages/next/types/react-dom.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ declare module 'react-dom/server.edge' {
7070
>
7171
}
7272

73+
declare module 'react-dom/server.browser' {
74+
export * from 'react-dom/server.edge'
75+
}
76+
7377
declare module 'react-dom/static.edge' {
7478
import type { JSX } from 'react'
7579
/**

packages/next/webpack.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const pagesExternals = [
1313
'react-dom/package.json',
1414
'react-dom/client',
1515
'react-dom/server',
16+
'react-dom/server.browser',
1617
'react-dom/server.edge',
1718
'react-server-dom-webpack/client',
1819
'react-server-dom-webpack/client.edge',

0 commit comments

Comments
 (0)