Skip to content

Commit

Permalink
Extend support of Pages router to React 18
Browse files Browse the repository at this point in the history
  • Loading branch information
eps1lon committed Sep 18, 2024
1 parent 7d40d87 commit d2f3767
Show file tree
Hide file tree
Showing 20 changed files with 343 additions and 131 deletions.
6 changes: 5 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@
"jest/no-conditional-expect": "off",
"jest/valid-title": "off",
"jest/no-interpolation-in-snapshots": "off",
"jest/no-export": "off"
"jest/no-export": "off",
"jest/no-standalone-expect": [
"error",
{ "additionalTestBlockFunctions": ["gateReact18"] }
]
}
},
{ "files": ["**/__tests__/**"], "env": { "jest": true } },
Expand Down
4 changes: 2 additions & 2 deletions packages/next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@
"@opentelemetry/api": "^1.1.0",
"@playwright/test": "^1.41.2",
"babel-plugin-react-compiler": "*",
"react": "19.0.0-rc-a99d8e8d-20240916",
"react-dom": "19.0.0-rc-a99d8e8d-20240916",
"react": "^18.2.0 || 19.0.0-rc-a99d8e8d-20240916",
"react-dom": "^18.2.0 || 19.0.0-rc-a99d8e8d-20240916",
"sass": "^1.3.0"
},
"peerDependenciesMeta": {
Expand Down
4 changes: 2 additions & 2 deletions packages/next/src/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ const NEXT_PROJECT_ROOT_DIST_CLIENT = path.join(
'client'
)

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

export const babelIncludeRegexes: RegExp[] = [
Expand Down
39 changes: 39 additions & 0 deletions packages/next/src/client/legacy/image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import React, {
useState,
type JSX,
} from 'react'
import * as ReactDOM from 'react-dom'
import Head from '../../shared/lib/head'
import {
imageConfigDefault,
VALID_LOADERS,
Expand All @@ -26,6 +28,8 @@ function normalizeSrc(src: string): string {
return src[0] === '/' ? src.slice(1) : src
}

const supportsFloat = typeof ReactDOM.preload === 'function'

const configEnv = process.env.__NEXT_IMAGE_OPTS as any as ImageConfigComplete
const loadedImageURLs = new Set<string>()
const allImgs = new Map<
Expand Down Expand Up @@ -978,6 +982,20 @@ export default function Image({
}
}

const linkProps:
| React.DetailedHTMLProps<
React.LinkHTMLAttributes<HTMLLinkElement>,
HTMLLinkElement
>
| undefined = supportsFloat
? undefined
: {
imageSrcSet: imgAttributes.srcSet,
imageSizes: imgAttributes.sizes,
crossOrigin: rest.crossOrigin,
referrerPolicy: rest.referrerPolicy,
}

const useLayoutEffect =
typeof window === 'undefined' ? React.useEffect : React.useLayoutEffect
const onLoadingCompleteRef = useRef(onLoadingComplete)
Expand Down Expand Up @@ -1044,6 +1062,27 @@ export default function Image({
) : null}
<ImageElement {...imgElementArgs} />
</span>
{!supportsFloat && priority ? (
// Note how we omit the `href` attribute, as it would only be relevant
// for browsers that do not support `imagesrcset`, and in those cases
// it would likely cause the incorrect image to be preloaded.
//
// https://html.spec.whatwg.org/multipage/semantics.html#attr-link-imagesrcset
<Head>
<link
key={
'__nimg-' +
imgAttributes.src +
imgAttributes.srcSet +
imgAttributes.sizes
}
rel="preload"
as="image"
href={imgAttributes.srcSet ? undefined : imgAttributes.src}
{...linkProps}
/>
</Head>
) : null}
</>
)
}
41 changes: 23 additions & 18 deletions packages/next/src/client/use-merged-ref.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,34 @@
import { useMemo, type Ref } from 'react'
import { useMemo, useRef, type Ref } from 'react'

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

export function mergeRefs<TElement>(
refA: Ref<TElement>,
refB: Ref<TElement>
): Ref<TElement> {
if (!refA || !refB) {
return refA || refB
}

return (current: TElement) => {
const cleanupA = applyRef(refA, current)
const cleanupB = applyRef(refB, current)
return useMemo(() => {
if (!refA || !refB) {
return refA || refB
}

return () => {
cleanupA()
cleanupB()
return (current: TElement | null): void => {
if (current === null) {
cleanupA.current()
cleanupB.current()
} else {
cleanupA.current = applyRef(refA, current)
cleanupB.current = applyRef(refB, current)
}
}
}
}, [refA, refB])
}

function applyRef<TElement>(
Expand Down
7 changes: 4 additions & 3 deletions packages/next/src/server/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import type { Revalidate, SwrDelta } from './lib/revalidate'
import type { COMPILER_NAMES } from '../shared/lib/constants'

import React, { type JSX } from 'react'
import ReactDOMServerEdge from 'react-dom/server.edge'
import ReactDOMServerBrowser from 'react-dom/server.browser'
import { StyleRegistry, createStyleRegistry } from 'styled-jsx'
import {
GSP_NO_RETURNED_VALUE,
Expand Down Expand Up @@ -127,7 +127,8 @@ function noRouter() {
}

async function renderToString(element: React.ReactElement) {
const renderStream = await ReactDOMServerEdge.renderToReadableStream(element)
const renderStream =
await ReactDOMServerBrowser.renderToReadableStream(element)
await renderStream.allReady
return streamToString(renderStream)
}
Expand Down Expand Up @@ -1326,7 +1327,7 @@ export async function renderToHTMLImpl(
) => {
const content = renderContent(EnhancedApp, EnhancedComponent)
return await renderToInitialFizzStream({
ReactDOMServer: ReactDOMServerEdge,
ReactDOMServer: ReactDOMServerBrowser,
element: content,
})
}
Expand Down
4 changes: 4 additions & 0 deletions packages/next/types/react-dom.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ declare module 'react-dom/server.edge' {
>
}

declare module 'react-dom/server.browser' {
export * from 'react-dom/server.edge'
}

declare module 'react-dom/static.edge' {
import type { JSX } from 'react'
/**
Expand Down
1 change: 1 addition & 0 deletions packages/next/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const pagesExternals = [
'react-dom/package.json',
'react-dom/client',
'react-dom/server',
'react-dom/server.browser',
'react-dom/server.edge',
'react-server-dom-webpack/client',
'react-server-dom-webpack/client.edge',
Expand Down
Loading

0 comments on commit d2f3767

Please sign in to comment.