Skip to content

Commit

Permalink
Revert "Revert "Support React 18 in Pages Router" (#69911)"
Browse files Browse the repository at this point in the history
This reverts commit 69f07b6.
  • Loading branch information
ztanner authored Sep 11, 2024
1 parent 298d930 commit fa2569a
Show file tree
Hide file tree
Showing 38 changed files with 581 additions and 341 deletions.
4 changes: 2 additions & 2 deletions examples/reproduction-template/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
},
"dependencies": {
"next": "canary",
"react": "19.0.0-rc-7771d3a7-20240827",
"react-dom": "19.0.0-rc-7771d3a7-20240827"
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/node": "20.12.12",
Expand Down
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -206,10 +206,10 @@
"pretty-bytes": "5.3.0",
"pretty-ms": "7.0.0",
"random-seed": "0.3.0",
"react": "19.0.0-rc-7771d3a7-20240827",
"react": "18.3.1",
"react-17": "npm:react@17.0.2",
"react-builtin": "npm:react@19.0.0-rc-7771d3a7-20240827",
"react-dom": "19.0.0-rc-7771d3a7-20240827",
"react-dom": "18.3.1",
"react-dom-17": "npm:react-dom@17.0.2",
"react-dom-builtin": "npm:react-dom@19.0.0-rc-7771d3a7-20240827",
"react-dom-experimental-builtin": "npm:react-dom@0.0.0-experimental-7771d3a7-20240827",
Expand Down Expand Up @@ -269,10 +269,10 @@
"@babel/traverse": "7.22.5",
"@types/react": "npm:types-react@19.0.0-rc.0",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.0",
"react": "19.0.0-rc-7771d3a7-20240827",
"react-dom": "19.0.0-rc-7771d3a7-20240827",
"react-is": "19.0.0-rc-7771d3a7-20240827",
"scheduler": "0.25.0-rc-7771d3a7-20240827"
"react": "18.3.1",
"react-dom": "18.3.1",
"react-is": "18.3.1",
"scheduler": "0.23.2"
},
"patchedDependencies": {
"webpack-sources@3.2.3": "patches/webpack-sources@3.2.3.patch"
Expand Down
2 changes: 1 addition & 1 deletion packages/create-next-app/templates/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { GetTemplateFileArgs, InstallTemplateArgs } from "./types";

// Do not rename or format. sync-react script relies on this line.
// prettier-ignore
const nextjsReactPeerVersion = "19.0.0-rc-7771d3a7-20240827";
const nextjsReactPeerVersion = "^18.2.0";

/**
* Get the file path for a given file in a template, e.g. "next.config.js".
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-7771d3a7-20240827",
"react-dom": "19.0.0-rc-7771d3a7-20240827",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"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
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,6 @@ export function Errors({
)

const errorDetails: HydrationErrorState = (error as any).details || {}
const notes = errorDetails.notes || ''
const [warningTemplate, serverContent, clientContent] =
errorDetails.warning || [null, '', '']

Expand All @@ -252,6 +251,7 @@ export function Errors({
.replace(/^Warning: /, '')
.replace(/^Error: /, '')
: null
const notes = isAppDir ? errorDetails.notes || '' : hydrationWarning

return (
<Overlay>
Expand Down Expand Up @@ -307,7 +307,9 @@ export function Errors({
{/* If there's hydration warning, skip displaying the error name */}
{hydrationWarning ? '' : error.name + ': '}
<HotlinkedText
text={hydrationWarning || error.message}
text={
isAppDir ? hydrationWarning || error.message : error.message
}
matcher={isNextjsLink}
/>
</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export function PseudoHtmlDiff({
firstContent: string
secondContent: string
reactOutputComponentDiff: string | undefined
hydrationMismatchType: 'tag' | 'text'
hydrationMismatchType: 'tag' | 'text' | 'text-in-tag'
} & React.HTMLAttributes<HTMLPreElement>) {
const isHtmlTagsWarning = hydrationMismatchType === 'tag'
const isReactHydrationDiff = !!reactOutputComponentDiff
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,42 @@ const htmlTagsWarnings = new Set([
"In HTML, whitespace text nodes cannot be a child of <%s>. Make sure you don't have any extra whitespace between tags on each line of your source code.\nThis will cause a hydration error.",
])

export const getHydrationWarningType = (msg: NullableText): 'tag' | 'text' => {
// In React 18, the warning message is prefixed with "Warning: "
const normalizeWarningMessage = (msg: string) => msg.replace(/^Warning: /, '')

// Note: React 18 only
const textAndTagsMismatchWarnings = new Set([
'Warning: Expected server HTML to contain a matching text node for "%s" in <%s>.%s',
'Warning: Did not expect server HTML to contain the text node "%s" in <%s>.%s',
])

// Note: React 18 only
const textMismatchWarning =
'Warning: Text content did not match. Server: "%s" Client: "%s"%s'

const isTextMismatchWarning = (msg: NullableText) => textMismatchWarning === msg
const isTextInTagsMismatchWarning = (msg: NullableText) =>
Boolean(msg && textAndTagsMismatchWarnings.has(msg))

export const getHydrationWarningType = (
msg: NullableText
): 'tag' | 'text' | 'text-in-tag' => {
if (isHtmlTagsWarning(msg)) return 'tag'
return 'text'
}

const isHtmlTagsWarning = (msg: NullableText) =>
Boolean(msg && htmlTagsWarnings.has(msg))
const isHtmlTagsWarning = (msg: NullableText) => {
if (msg && typeof msg === 'string') {
return htmlTagsWarnings.has(normalizeWarningMessage(msg))
}

return false
}

const isKnownHydrationWarning = (msg: NullableText) => isHtmlTagsWarning(msg)
const isKnownHydrationWarning = (msg: NullableText) =>
isHtmlTagsWarning(msg) ||
isTextInTagsMismatchWarning(msg) ||
isTextMismatchWarning(msg)

export const getReactHydrationDiffSegments = (msg: NullableText) => {
if (msg) {
Expand All @@ -51,14 +78,18 @@ export const getReactHydrationDiffSegments = (msg: NullableText) => {
export function storeHydrationErrorStateFromConsoleArgs(...args: any[]) {
const [msg, serverContent, clientContent, componentStack] = args
if (isKnownHydrationWarning(msg)) {
hydrationErrorState.warning = [
// remove the last %s from the message
msg,
serverContent,
clientContent,
]
hydrationErrorState.warning = [msg, serverContent, clientContent]
hydrationErrorState.componentStack = componentStack
hydrationErrorState.serverContent = serverContent
hydrationErrorState.clientContent = clientContent

return [
...args,
// We tack on the hydration error message to the console.error message so that
// it matches the error we display in the redbox overlay
`\nSee more info here: https://nextjs.org/docs/messages/react-hydration-error`,
]
}

return args
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,13 @@ function handleError(error: unknown) {

let origConsoleError = console.error
function nextJsHandleConsoleError(...args: any[]) {
// To support React 19, this will need to be updated as follows:
// const error = process.env.NODE_ENV !== 'production' ? args[1] : args[0]
// See https://github.com/facebook/react/blob/d50323eb845c5fde0d720cae888bf35dedd05506/packages/react-reconciler/src/ReactFiberErrorLogger.js#L78
const error = process.env.NODE_ENV !== 'production' ? args[1] : args[0]
storeHydrationErrorStateFromConsoleArgs(...args)
const error = args[0]
const errorArgs = storeHydrationErrorStateFromConsoleArgs(...args)
handleError(error)
origConsoleError.apply(window.console, args)
origConsoleError.apply(window.console, errorArgs)
}

function onUnhandledError(event: ErrorEvent) {
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
2 changes: 1 addition & 1 deletion packages/next/src/compiled/unistore/unistore.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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 @@ -1325,7 +1326,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 fa2569a

Please sign in to comment.