diff --git a/next-env.d.ts b/next-env.d.ts index 52e831b4342..36a4fe488ad 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,5 +1,7 @@ /// /// +/// +/// // NOTE: This file should not be edited -// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/next.config.js b/next.config.js index fe88a09a0c4..0dab6fe28cd 100644 --- a/next.config.js +++ b/next.config.js @@ -72,6 +72,12 @@ const nextConfig = { return config; }, + serverExternalPackages: [ + 'eslint', + '@babel/core', + '@babel/preset-react', + '@babel/plugin-transform-modules-commonjs', + ], }; module.exports = nextConfig; diff --git a/package.json b/package.json index 55fcc0a5b71..8fab98f28aa 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "classnames": "^2.2.6", "debounce": "^1.2.1", "github-slugger": "^1.3.0", - "next": "15.1.11", + "next": "^15.4.0", "next-remote-watch": "^1.0.0", "parse-numeric-range": "^1.2.0", "react": "^19.0.0", diff --git a/src/app/errors/[errorCode]/lib/fetch-error-codes.ts b/src/app/errors/[errorCode]/lib/fetch-error-codes.ts new file mode 100644 index 00000000000..cedb99d90ca --- /dev/null +++ b/src/app/errors/[errorCode]/lib/fetch-error-codes.ts @@ -0,0 +1,9 @@ +import {unstable_cache} from 'next/cache'; + +export const fetchReactErrorCodes = unstable_cache(async () => { + return ( + await fetch( + 'https://raw.githubusercontent.com/facebook/react/main/scripts/error-codes/codes.json' + ) + ).json() as Promise<{[key: string]: string}>; +}, ['react-error-codes']); diff --git a/src/app/errors/[errorCode]/page.tsx b/src/app/errors/[errorCode]/page.tsx new file mode 100644 index 00000000000..b329021a154 --- /dev/null +++ b/src/app/errors/[errorCode]/page.tsx @@ -0,0 +1,55 @@ +import {unstable_cache} from 'next/cache'; +import {notFound} from 'next/navigation'; +import ErrorDecoderPage from '../components/error-decoder-page'; +import type {Metadata, ResolvingMetadata} from 'next'; + +interface ErrorDecoderPageProps { + params: Promise<{errorCode: string}>; +} + +export default async function ErrorPage({params}: ErrorDecoderPageProps) { + const {errorCode} = await params; + const errorCodes = await fetchReactErrorCodes(); + + if (errorCode && !(errorCode in errorCodes)) { + notFound(); + } + + return ; +} + +const fetchReactErrorCodes = unstable_cache(async () => { + return ( + await fetch( + 'https://raw.githubusercontent.com/facebook/react/main/scripts/error-codes/codes.json' + ) + ).json() as Promise<{[key: string]: string}>; +}, ['react-error-codes']); + +export async function generateStaticParams(): Promise< + Array<{errorCode: string}> +> { + return Object.keys(await fetchReactErrorCodes()).map((code) => ({ + errorCode: code, + })); +} + +export async function generateMetadata( + {params}: ErrorDecoderPageProps, + parent: Promise +): Promise { + const {errorCode} = await params; + const resolvedParent = await parent; + const parentOpenGraph = resolvedParent ? resolvedParent.openGraph : {}; + + return { + title: `Minified React error #${errorCode}`, + alternates: { + canonical: `./`, + }, + openGraph: { + ...parentOpenGraph, + title: `Minified React error #${errorCode}`, + }, + }; +} diff --git a/src/app/errors/components/error-decoder-page.tsx b/src/app/errors/components/error-decoder-page.tsx new file mode 100644 index 00000000000..57cec2d65af --- /dev/null +++ b/src/app/errors/components/error-decoder-page.tsx @@ -0,0 +1,64 @@ +import fsp from 'node:fs/promises'; +import fs from 'node:fs'; +import {Page} from '../../../components/Layout/Page'; +import sidebarLearn from 'sidebarLearn.json'; +import type {RouteItem} from '../../../components/Layout/getRouteMeta'; +import {notFound} from 'next/navigation'; +import {join} from 'node:path'; +import compileMDX from '../../../utils/compileMDX'; +import ErrorMDX from './error-mdx'; + +export default async function ErrorDecoderPage({ + errorCode, + errorCodes, +}: { + errorCode: string | null; + errorCodes: {[key: string]: string}; +}) { + if (errorCode && !errorCodes[errorCode]) { + notFound(); + } + + const rootDir = join(process.cwd(), 'src', 'content', 'errors'); + let path = errorCode || 'index'; + let mdx; + if (fs.existsSync(join(rootDir, path + '.md'))) { + mdx = await fsp.readFile(join(rootDir, path + '.md'), 'utf8'); + } else { + mdx = await fsp.readFile(join(rootDir, 'generic.md'), 'utf8'); + } + + const {content} = await compileMDX(mdx, path, {code: errorCode, errorCodes}); + const errorMessage = errorCode ? errorCodes[errorCode] : null; + + return ( + +
+ +
+ {/* +

+ We highly recommend using the development build locally when debugging + your app since it tracks additional debug info and provides helpful + warnings about potential problems in your apps, but if you encounter + an exception while using the production build, this page will + reassemble the original error message. +

+ +
*/} +
+ ); +} diff --git a/src/app/errors/components/error-mdx.tsx b/src/app/errors/components/error-mdx.tsx new file mode 100644 index 00000000000..50a74d7a3f1 --- /dev/null +++ b/src/app/errors/components/error-mdx.tsx @@ -0,0 +1,48 @@ +'use client'; + +import {Fragment} from 'react'; +import {ErrorDecoderProvider} from '../../../components/_/ErrorDecoderContext'; +import {MDXComponents} from '../../../components/MDX/MDXComponents'; + +export default function ErrorMDX({ + content, + errorCode, + errorMessage, +}: { + content: string; + errorCode: string | null; + errorMessage: string | null; +}) { + return ( + + {JSON.parse(content, reviveNodeOnClient)} + + ); +} + +// Deserialize a client React tree from JSON. +function reviveNodeOnClient(parentPropertyName: unknown, val: any) { + if (Array.isArray(val) && val[0] == '$r') { + // Assume it's a React element. + let Type = val[1]; + let key = val[2]; + if (key == null) { + key = parentPropertyName; // Index within a parent. + } + let props = val[3]; + if (Type === 'wrapper') { + Type = Fragment; + props = {children: props.children}; + } + if (Type in MDXComponents) { + Type = MDXComponents[Type as keyof typeof MDXComponents]; + } + if (!Type) { + console.error('Unknown type: ' + Type); + Type = Fragment; + } + return ; + } else { + return val; + } +} diff --git a/src/app/errors/page.tsx b/src/app/errors/page.tsx new file mode 100644 index 00000000000..6b07daf59f7 --- /dev/null +++ b/src/app/errors/page.tsx @@ -0,0 +1,28 @@ +import type {Metadata, ResolvingMetadata} from 'next'; +import {fetchReactErrorCodes} from './[errorCode]/lib/fetch-error-codes'; +import ErrorDecoderPage from './components/error-decoder-page'; + +export default async function ErrorPage() { + const errorCodes = await fetchReactErrorCodes(); + + return ; +} + +export async function generateMetadata( + _: unknown, + parent?: ResolvingMetadata +): Promise { + const resolvedParent = await parent; + const parentOpenGraph = resolvedParent ? resolvedParent.openGraph : {}; + + return { + title: 'Minified Error Decoder', + alternates: { + canonical: `./`, + }, + openGraph: { + ...parentOpenGraph, + title: 'Minified Error Decoder', + }, + }; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 00000000000..003887cf65d --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,68 @@ +import type {Metadata, Viewport} from 'next'; +import {SharedRootBody, SharedRootHead} from '../components/_/root-layout'; +import {siteConfig} from '../siteConfig'; +import {preload} from 'react-dom'; + +import '@docsearch/css'; +import '../styles/algolia.css'; +import '../styles/index.css'; +import '../styles/sandpack.css'; + +export default function RootLayout({children}: React.PropsWithChildren) { + [ + 'https://react.dev/fonts/Source-Code-Pro-Regular.woff2', + 'https://react.dev/fonts/Source-Code-Pro-Bold.woff2', + 'https://react.dev/fonts/Optimistic_Display_W_Md.woff2', + 'https://react.dev/fonts/Optimistic_Display_W_SBd.woff2', + 'https://react.dev/fonts/Optimistic_Display_W_Bd.woff2', + 'https://react.dev/fonts/Optimistic_Text_W_Md.woff2', + 'https://react.dev/fonts/Optimistic_Text_W_Bd.woff2', + 'https://react.dev/fonts/Optimistic_Text_W_Rg.woff2', + 'https://react.dev/fonts/Optimistic_Text_W_It.woff2', + ].forEach((href) => { + preload(href, {as: 'font', type: 'font/woff2', crossOrigin: 'anonymous'}); + }); + + return ( + + + + + {children} + + ); +} + +export const viewport: Viewport = { + width: 'device-width', + initialScale: 1, +}; + +export const metadata: Metadata = { + metadataBase: new URL('https://' + getDomain(siteConfig.languageCode)), + alternates: { + canonical: './', + }, + openGraph: { + type: 'website', + url: './', + images: ['/images/og-default.png'], + }, + twitter: { + card: 'summary_large_image', + site: '@reactjs', + creator: '@reactjs', + images: ['/images/og-default.png'], + }, + facebook: { + appId: '623268441017527', + }, +}; + +function getDomain(languageCode: string): string { + const subdomain = languageCode === 'en' ? '' : languageCode + '.'; + return subdomain + 'react.dev'; +} diff --git a/src/components/Layout/Feedback.tsx b/src/components/Layout/Feedback.tsx index fe927251709..752cf5f30ee 100644 --- a/src/components/Layout/Feedback.tsx +++ b/src/components/Layout/Feedback.tsx @@ -1,3 +1,5 @@ +'use client'; + /** * Copyright (c) Meta Platforms, Inc. and affiliates. * @@ -10,14 +12,13 @@ */ import {useState} from 'react'; -import {useRouter} from 'next/router'; +import {usePathname} from 'next/navigation'; import cn from 'classnames'; export function Feedback({onSubmit = () => {}}: {onSubmit?: () => void}) { - const {asPath} = useRouter(); - const cleanedPath = asPath.split(/[\?\#]/)[0]; + const pathname = usePathname(); // Reset on route changes. - return ; + return ; } const thumbsUpIcon = ( diff --git a/src/components/Layout/HomeContent.js b/src/components/Layout/HomeContent.js index f9b785db420..251f2cab6d6 100644 --- a/src/components/Layout/HomeContent.js +++ b/src/components/Layout/HomeContent.js @@ -1,3 +1,5 @@ +'use client'; + /** * Copyright (c) Meta Platforms, Inc. and affiliates. * diff --git a/src/components/Layout/Page.tsx b/src/components/Layout/Page.tsx index aa39fe5fc2b..50b03b31b2d 100644 --- a/src/components/Layout/Page.tsx +++ b/src/components/Layout/Page.tsx @@ -9,9 +9,11 @@ * Copyright (c) Facebook, Inc. and its affiliates. */ +'use client'; + import {Suspense} from 'react'; import * as React from 'react'; -import {useRouter} from 'next/router'; +import {useRouter} from 'next/compat/router'; import {SidebarNav} from './SidebarNav'; import {Footer} from './Footer'; import {Toc} from './Toc'; @@ -28,6 +30,7 @@ import {HomeContent} from './HomeContent'; import {TopNav} from './TopNav'; import cn from 'classnames'; import Head from 'next/head'; +import {usePathname} from 'next/navigation'; import(/* webpackPrefetch: true */ '../MDX/CodeBlock/CodeBlock'); @@ -43,6 +46,8 @@ interface PageProps { }; section: 'learn' | 'reference' | 'community' | 'blog' | 'home' | 'unknown'; languages?: Languages | null; + /** Should this being used with the Next.js App Router? */ + appRouter?: boolean; } export function Page({ @@ -52,9 +57,14 @@ export function Page({ meta, section, languages = null, + appRouter = false, }: PageProps) { - const {asPath} = useRouter(); - const cleanedPath = asPath.split(/[\?\#]/)[0]; + const pagesRouter = useRouter(); + const pathname = usePathname()!; + const cleanedPath = pagesRouter + ? pagesRouter.asPath.split(/[\?\#]/)[0] + : pathname; + const {route, nextRoute, prevRoute, breadcrumbs, order} = getRouteMeta( cleanedPath, routeTree @@ -125,13 +135,15 @@ export function Page({ return ( <> - + {!appRouter && ( + + )} {(isHomePage || isBlogIndex) && (
+ key={cleanedPath}> {content}
- {showToc && toc.length > 0 && } + {showToc && toc.length > 0 && ( + + )}
diff --git a/src/components/Layout/Sidebar/SidebarRouteTree.tsx b/src/components/Layout/Sidebar/SidebarRouteTree.tsx index 863355bfdc8..30480cbfcbe 100644 --- a/src/components/Layout/Sidebar/SidebarRouteTree.tsx +++ b/src/components/Layout/Sidebar/SidebarRouteTree.tsx @@ -1,3 +1,5 @@ +'use client'; + /** * Copyright (c) Meta Platforms, Inc. and affiliates. * @@ -12,12 +14,12 @@ import {useRef, useLayoutEffect, Fragment} from 'react'; import cn from 'classnames'; -import {useRouter} from 'next/router'; import {SidebarLink} from './SidebarLink'; import {useCollapse} from 'react-collapsed'; import usePendingRoute from 'hooks/usePendingRoute'; import type {RouteItem} from 'components/Layout/getRouteMeta'; import {siteConfig} from 'siteConfig'; +import {usePathname} from 'next/navigation'; interface SidebarRouteTreeProps { isForceExpanded: boolean; @@ -84,7 +86,7 @@ export function SidebarRouteTree({ routeTree, level = 0, }: SidebarRouteTreeProps) { - const slug = useRouter().asPath.split(/[\?\#]/)[0]; + const slug = usePathname(); const pendingRoute = usePendingRoute(); const currentRoutes = routeTree.routes as RouteItem[]; return ( diff --git a/src/components/Layout/TopNav/TopNav.tsx b/src/components/Layout/TopNav/TopNav.tsx index 148098933d5..cb2fe2ce8d9 100644 --- a/src/components/Layout/TopNav/TopNav.tsx +++ b/src/components/Layout/TopNav/TopNav.tsx @@ -1,3 +1,5 @@ +'use client'; + /** * Copyright (c) Meta Platforms, Inc. and affiliates. * @@ -21,7 +23,6 @@ import Image from 'next/image'; import * as React from 'react'; import cn from 'classnames'; import NextLink from 'next/link'; -import {useRouter} from 'next/router'; import {disableBodyScroll, enableBodyScroll} from 'body-scroll-lock'; import {IconClose} from 'components/Icon/IconClose'; @@ -34,6 +35,7 @@ import {SidebarRouteTree} from '../Sidebar'; import type {RouteItem} from '../getRouteMeta'; import {siteConfig} from 'siteConfig'; import BrandMenu from './BrandMenu'; +import {usePathname} from 'next/navigation'; declare global { interface Window { @@ -169,7 +171,7 @@ export default function TopNav({ const [showSearch, setShowSearch] = useState(false); const [isScrolled, setIsScrolled] = useState(false); const scrollParentRef = useRef(null); - const {asPath} = useRouter(); + const pathname = usePathname(); // HACK. Fix up the data structures instead. if ((routeTree as any).routes.length === 1) { @@ -190,7 +192,7 @@ export default function TopNav({ // Close the overlay on any navigation. useEffect(() => { setIsMenuOpen(false); - }, [asPath]); + }, [pathname]); // Also close the overlay if the window gets resized past mobile layout. // (This is also important because we don't want to keep the body locked!) diff --git a/src/components/MDX/Challenges/Challenges.tsx b/src/components/MDX/Challenges/Challenges.tsx index 1b5dcfb1f06..5135f763135 100644 --- a/src/components/MDX/Challenges/Challenges.tsx +++ b/src/components/MDX/Challenges/Challenges.tsx @@ -16,7 +16,7 @@ import {H2} from 'components/MDX/Heading'; import {H4} from 'components/MDX/Heading'; import {Challenge} from './Challenge'; import {Navigation} from './Navigation'; -import {useRouter} from 'next/router'; +import {useHash} from '../../../hooks/useHash'; interface ChallengesProps { children: React.ReactElement[]; @@ -97,12 +97,12 @@ export function Challenges({ const queuedScrollRef = useRef(QueuedScroll.INIT); const [activeIndex, setActiveIndex] = useState(0); const currentChallenge = challenges[activeIndex]; - const {asPath} = useRouter(); + const hash = useHash(); useEffect(() => { if (queuedScrollRef.current === QueuedScroll.INIT) { const initIndex = challenges.findIndex( - (challenge) => challenge.id === asPath.split('#')[1] + (challenge) => challenge.id === hash ); if (initIndex === -1) { queuedScrollRef.current = undefined; @@ -119,7 +119,7 @@ export function Challenges({ }); queuedScrollRef.current = undefined; } - }, [activeIndex, asPath, challenges]); + }, [activeIndex, challenges, hash]); const handleChallengeChange = (index: number) => { setActiveIndex(index); diff --git a/src/components/MDX/Challenges/index.tsx b/src/components/MDX/Challenges/index.tsx index 27e3df1ef0d..481f154ac7f 100644 --- a/src/components/MDX/Challenges/index.tsx +++ b/src/components/MDX/Challenges/index.tsx @@ -1,3 +1,5 @@ +'use client'; + /** * Copyright (c) Meta Platforms, Inc. and affiliates. * diff --git a/src/components/MDX/ErrorDecoder.tsx b/src/components/MDX/ErrorDecoder.tsx index 423790198bf..ea9355fd371 100644 --- a/src/components/MDX/ErrorDecoder.tsx +++ b/src/components/MDX/ErrorDecoder.tsx @@ -1,3 +1,5 @@ +'use client'; + /** * Copyright (c) Meta Platforms, Inc. and affiliates. * @@ -6,7 +8,7 @@ */ import {useEffect, useState} from 'react'; -import {useErrorDecoderParams} from '../ErrorDecoderContext'; +import {useErrorDecoderParams} from '../_/ErrorDecoderContext'; import cn from 'classnames'; function replaceArgs( diff --git a/src/components/MDX/ExpandableExample.tsx b/src/components/MDX/ExpandableExample.tsx index c11cd6c3ccc..528aac92084 100644 --- a/src/components/MDX/ExpandableExample.tsx +++ b/src/components/MDX/ExpandableExample.tsx @@ -1,3 +1,5 @@ +'use client'; + /** * Copyright (c) Meta Platforms, Inc. and affiliates. * @@ -16,8 +18,8 @@ import {IconDeepDive} from '../Icon/IconDeepDive'; import {IconCodeBlock} from '../Icon/IconCodeBlock'; import {Button} from '../Button'; import {H4} from './Heading'; -import {useRouter} from 'next/router'; import {useEffect, useRef, useState} from 'react'; +import {useHash} from '../../hooks/useHash'; interface ExpandableExampleProps { children: React.ReactNode; @@ -35,8 +37,8 @@ function ExpandableExample({children, excerpt, type}: ExpandableExampleProps) { const isExample = type === 'Example'; const id = children[0].props.id; - const {asPath} = useRouter(); - const shouldAutoExpand = id === asPath.split('#')[1]; + const hash = useHash(); + const shouldAutoExpand = !!hash && id === hash.split('#')[1]; const queuedExpandRef = useRef(shouldAutoExpand); const [isExpanded, setIsExpanded] = useState(false); diff --git a/src/components/MDX/Illustration/AuthorCredit.tsx b/src/components/MDX/Illustration/AuthorCredit.tsx new file mode 100644 index 00000000000..2f230ef8cad --- /dev/null +++ b/src/components/MDX/Illustration/AuthorCredit.tsx @@ -0,0 +1,28 @@ +export function AuthorCredit({ + author = 'Rachel Lee Nabors', + authorLink = 'https://nearestnabors.com/', +}: { + author: string; + authorLink: string; +}) { + return ( +
+

+ + Illustrated by{' '} + {authorLink ? ( + + {author} + + ) : ( + author + )} + +

+
+ ); +} diff --git a/src/components/MDX/Illustration/Illustration.tsx b/src/components/MDX/Illustration/Illustration.tsx new file mode 100644 index 00000000000..96ba3a89244 --- /dev/null +++ b/src/components/MDX/Illustration/Illustration.tsx @@ -0,0 +1,40 @@ +'use client'; + +import {useContext} from 'react'; +import {IllustrationContext} from './IllustrationContext'; +import {AuthorCredit} from './AuthorCredit'; + +export function Illustration({ + caption, + src, + alt, + author, + authorLink, +}: { + caption: string; + src: string; + alt: string; + author: string; + authorLink: string; +}) { + const {isInBlock} = useContext(IllustrationContext); + + return ( +
+
+ {alt} + {caption ? ( +
+ {caption} +
+ ) : null} +
+ {!isInBlock && } +
+ ); +} diff --git a/src/components/MDX/Illustration/IllustrationBlock.tsx b/src/components/MDX/Illustration/IllustrationBlock.tsx new file mode 100644 index 00000000000..2538bec6197 --- /dev/null +++ b/src/components/MDX/Illustration/IllustrationBlock.tsx @@ -0,0 +1,56 @@ +import {Children} from 'react'; +import {IllustrationContext} from './IllustrationContext'; +import {AuthorCredit} from './AuthorCredit'; + +const isInBlockTrue = {isInBlock: true}; + +export function IllustrationBlock({ + sequential, + author, + authorLink, + children, +}: { + author: string; + authorLink: string; + sequential: boolean; + children: any; +}) { + const imageInfos = Children.toArray(children).map( + (child: any) => child.props + ); + const images = imageInfos.map((info, index) => ( +
+
+ {info.alt} +
+ {info.caption ? ( +
+ {info.caption} +
+ ) : null} +
+ )); + return ( + +
+ {sequential ? ( +
    + {images.map((x: any, i: number) => ( +
  1. + {x} +
  2. + ))} +
+ ) : ( +
{images}
+ )} + +
+
+ ); +} diff --git a/src/components/MDX/Illustration/IllustrationContext.ts b/src/components/MDX/Illustration/IllustrationContext.ts new file mode 100644 index 00000000000..226d0d4b16f --- /dev/null +++ b/src/components/MDX/Illustration/IllustrationContext.ts @@ -0,0 +1,9 @@ +'use client'; + +import {createContext} from 'react'; + +export const IllustrationContext = createContext<{ + isInBlock?: boolean; +}>({ + isInBlock: false, +}); diff --git a/src/components/MDX/LanguageList.tsx b/src/components/MDX/LanguageList.tsx new file mode 100644 index 00000000000..258003058f0 --- /dev/null +++ b/src/components/MDX/LanguageList.tsx @@ -0,0 +1,39 @@ +'use client'; + +import {useContext} from 'react'; +import {LanguagesContext} from './LanguagesContext'; +import {finishedTranslations} from 'utils/finishedTranslations'; +import Link from 'next/link'; +import {LI, UL} from './List'; + +type TranslationProgress = 'complete' | 'in-progress'; + +export function LanguageList({progress}: {progress: TranslationProgress}) { + const allLanguages = useContext(LanguagesContext) ?? []; + const languages = allLanguages + .filter( + ({code}) => + code !== 'en' && + (progress === 'complete' + ? finishedTranslations.includes(code) + : !finishedTranslations.includes(code)) + ) + .sort((a, b) => a.enName.localeCompare(b.enName)); + return ( +
    + {languages.map(({code, name, enName}) => { + return ( +
  • + + {enName} ({name}) + {' '} + —{' '} + + Contribute + +
  • + ); + })} +
+ ); +} diff --git a/src/components/MDX/LanguagesContext.tsx b/src/components/MDX/LanguagesContext.tsx index cd9f8881671..ac37dcc9f25 100644 --- a/src/components/MDX/LanguagesContext.tsx +++ b/src/components/MDX/LanguagesContext.tsx @@ -1,3 +1,5 @@ +'use client'; + /** * Copyright (c) Meta Platforms, Inc. and affiliates. * diff --git a/src/components/MDX/List.tsx b/src/components/MDX/List.tsx new file mode 100644 index 00000000000..fab233fba59 --- /dev/null +++ b/src/components/MDX/List.tsx @@ -0,0 +1,9 @@ +export const OL = (p: React.HTMLAttributes) => ( +
    +); +export const LI = (p: React.HTMLAttributes) => ( +
  1. +); +export const UL = (p: React.HTMLAttributes) => ( +
      +); diff --git a/src/components/MDX/MDXComponents.tsx b/src/components/MDX/MDXComponents.tsx index a32dad27174..29a8e950c33 100644 --- a/src/components/MDX/MDXComponents.tsx +++ b/src/components/MDX/MDXComponents.tsx @@ -9,7 +9,7 @@ * Copyright (c) Facebook, Inc. and its affiliates. */ -import {Children, useContext, useMemo} from 'react'; +import {useContext, useMemo} from 'react'; import * as React from 'react'; import cn from 'classnames'; import type {HTMLAttributes} from 'react'; @@ -39,12 +39,14 @@ import ButtonLink from 'components/ButtonLink'; import {TocContext} from './TocContext'; import type {Toc, TocItem} from './TocContext'; import {TeamMember} from './TeamMember'; -import {LanguagesContext} from './LanguagesContext'; -import {finishedTranslations} from 'utils/finishedTranslations'; import ErrorDecoder from './ErrorDecoder'; import {IconCanary} from '../Icon/IconCanary'; import {IconExperimental} from 'components/Icon/IconExperimental'; +import {Illustration} from './Illustration/Illustration'; +import {IllustrationBlock} from './Illustration/IllustrationBlock'; +import {LanguageList} from './LanguageList'; +import {LI, OL, UL} from './List'; function CodeStep({children, step}: {children: any; step: number}) { return ( @@ -76,16 +78,6 @@ const Strong = (strong: HTMLAttributes) => ( ); -const OL = (p: HTMLAttributes) => ( -
        -); -const LI = (p: HTMLAttributes) => ( -
      1. -); -const UL = (p: HTMLAttributes) => ( -
          -); - const Divider = () => (
          ); @@ -262,129 +254,6 @@ function Recipes(props: any) { return ; } -function AuthorCredit({ - author = 'Rachel Lee Nabors', - authorLink = 'https://nearestnabors.com/', -}: { - author: string; - authorLink: string; -}) { - return ( -
          -

          - - Illustrated by{' '} - {authorLink ? ( - - {author} - - ) : ( - author - )} - -

          -
          - ); -} - -const IllustrationContext = React.createContext<{ - isInBlock?: boolean; -}>({ - isInBlock: false, -}); - -function Illustration({ - caption, - src, - alt, - author, - authorLink, -}: { - caption: string; - src: string; - alt: string; - author: string; - authorLink: string; -}) { - const {isInBlock} = React.useContext(IllustrationContext); - - return ( -
          -
          - {alt} - {caption ? ( -
          - {caption} -
          - ) : null} -
          - {!isInBlock && } -
          - ); -} - -const isInBlockTrue = {isInBlock: true}; - -function IllustrationBlock({ - sequential, - author, - authorLink, - children, -}: { - author: string; - authorLink: string; - sequential: boolean; - children: any; -}) { - const imageInfos = Children.toArray(children).map( - (child: any) => child.props - ); - const images = imageInfos.map((info, index) => ( -
          -
          - {info.alt} -
          - {info.caption ? ( -
          - {info.caption} -
          - ) : null} -
          - )); - return ( - -
          - {sequential ? ( -
            - {images.map((x: any, i: number) => ( -
          1. - {x} -
          2. - ))} -
          - ) : ( -
          {images}
          - )} - -
          -
          - ); -} - type NestedTocRoot = { item: null; children: Array; @@ -438,38 +307,6 @@ function InlineTocItem({items}: {items: Array}) { ); } -type TranslationProgress = 'complete' | 'in-progress'; - -function LanguageList({progress}: {progress: TranslationProgress}) { - const allLanguages = React.useContext(LanguagesContext) ?? []; - const languages = allLanguages - .filter( - ({code}) => - code !== 'en' && - (progress === 'complete' - ? finishedTranslations.includes(code) - : !finishedTranslations.includes(code)) - ) - .sort((a, b) => a.enName.localeCompare(b.enName)); - return ( -
            - {languages.map(({code, name, enName}) => { - return ( -
          • - - {enName} ({name}) - {' '} - —{' '} - - Contribute - -
          • - ); - })} -
          - ); -} - function YouTubeIframe(props: any) { return (
          diff --git a/src/components/MDX/Sandpack/CustomPreset.tsx b/src/components/MDX/Sandpack/CustomPreset.tsx index 4a241c87cbf..25dbbb41bc5 100644 --- a/src/components/MDX/Sandpack/CustomPreset.tsx +++ b/src/components/MDX/Sandpack/CustomPreset.tsx @@ -1,3 +1,5 @@ +'use client'; + /** * Copyright (c) Meta Platforms, Inc. and affiliates. * diff --git a/src/components/MDX/Sandpack/NavigationBar.tsx b/src/components/MDX/Sandpack/NavigationBar.tsx index 3fe743a2d24..6b3ebb941f6 100644 --- a/src/components/MDX/Sandpack/NavigationBar.tsx +++ b/src/components/MDX/Sandpack/NavigationBar.tsx @@ -1,3 +1,5 @@ +'use client'; + /** * Copyright (c) Meta Platforms, Inc. and affiliates. * diff --git a/src/components/MDX/Sandpack/runESLint.tsx b/src/components/MDX/Sandpack/runESLint.tsx index 667b22d7eb2..7c7c908eafd 100644 --- a/src/components/MDX/Sandpack/runESLint.tsx +++ b/src/components/MDX/Sandpack/runESLint.tsx @@ -1,3 +1,5 @@ +'use client'; + /** * Copyright (c) Meta Platforms, Inc. and affiliates. * diff --git a/src/components/MDX/Sandpack/useSandpackLint.tsx b/src/components/MDX/Sandpack/useSandpackLint.tsx index 479b53ee0df..9113bb232bf 100644 --- a/src/components/MDX/Sandpack/useSandpackLint.tsx +++ b/src/components/MDX/Sandpack/useSandpackLint.tsx @@ -1,3 +1,5 @@ +'use client'; + /** * Copyright (c) Meta Platforms, Inc. and affiliates. * diff --git a/src/components/MDX/TerminalBlock.tsx b/src/components/MDX/TerminalBlock.tsx index 0fd0160d665..66a34727d9c 100644 --- a/src/components/MDX/TerminalBlock.tsx +++ b/src/components/MDX/TerminalBlock.tsx @@ -1,3 +1,5 @@ +'use client'; + /** * Copyright (c) Meta Platforms, Inc. and affiliates. * diff --git a/src/components/MDX/TocContext.tsx b/src/components/MDX/TocContext.tsx index 924e6e09eed..296a1b106a8 100644 --- a/src/components/MDX/TocContext.tsx +++ b/src/components/MDX/TocContext.tsx @@ -1,3 +1,5 @@ +'use client'; + /** * Copyright (c) Meta Platforms, Inc. and affiliates. * diff --git a/src/components/Search.tsx b/src/components/Search.tsx index 24b066d70f4..402f03bb722 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -9,12 +9,11 @@ * Copyright (c) Facebook, Inc. and its affiliates. */ -import Head from 'next/head'; import Link from 'next/link'; -import Router from 'next/router'; +import {useRouter} from 'next/navigation'; import {lazy, useEffect} from 'react'; import * as React from 'react'; -import {createPortal} from 'react-dom'; +import {createPortal, preconnect} from 'react-dom'; import {siteConfig} from 'siteConfig'; import type {ComponentType, PropsWithChildren} from 'react'; import type {DocSearchModalProps} from '@docsearch/react/modal'; @@ -118,14 +117,13 @@ export function Search({ }, }: SearchProps) { useDocSearchKeyboardEvents({isOpen, onOpen, onClose}); + + const router = useRouter(); + + preconnect(`https://${options.appId}-dsn.algolia.net`); + return ( <> - - - {isOpen && createPortal( { diff --git a/src/components/Seo.tsx b/src/components/Seo.tsx index 90604102023..d9a1437ebb2 100644 --- a/src/components/Seo.tsx +++ b/src/components/Seo.tsx @@ -11,9 +11,9 @@ import * as React from 'react'; import Head from 'next/head'; -import {withRouter, Router} from 'next/router'; import {siteConfig} from '../siteConfig'; import {finishedTranslations} from 'utils/finishedTranslations'; +import {usePathname} from 'next/navigation'; export interface SeoProps { title: string; @@ -34,166 +34,158 @@ function getDomain(languageCode: string): string { return subdomain + 'react.dev'; } -export const Seo = withRouter( - ({ - title, - titleForTitleTag, - image = '/images/og-default.png', - router, - children, - isHomePage, - searchOrder, - }: SeoProps & {router: Router}) => { - const siteDomain = getDomain(siteConfig.languageCode); - const canonicalUrl = `https://${siteDomain}${ - router.asPath.split(/[\?\#]/)[0] - }`; - // Allow setting a different title for Google results - const pageTitle = - (titleForTitleTag ?? title) + (isHomePage ? '' : ' – React'); - // Twitter's meta parser is not very good. - const twitterTitle = pageTitle.replace(/[<>]/g, ''); - let description = isHomePage - ? 'React is the library for web and native user interfaces. Build user interfaces out of individual pieces called components written in JavaScript. React is designed to let you seamlessly combine components written by independent people, teams, and organizations.' - : 'The library for web and native user interfaces'; - return ( - - - {title != null && {pageTitle}} - {isHomePage && ( - // Let Google figure out a good description for each page. - - )} - +export function Seo({ + title, + titleForTitleTag, + image = '/images/og-default.png', + children, + isHomePage, + searchOrder, +}: SeoProps) { + const pathname = usePathname(); + const siteDomain = getDomain(siteConfig.languageCode); + const canonicalUrl = `https://${siteDomain}${pathname}`; + // Allow setting a different title for Google results + const pageTitle = + (titleForTitleTag ?? title) + (isHomePage ? '' : ' – React'); + // Twitter's meta parser is not very good. + const twitterTitle = pageTitle.replace(/[<>]/g, ''); + let description = isHomePage + ? 'React is the library for web and native user interfaces. Build user interfaces out of individual pieces called components written in JavaScript. React is designed to let you seamlessly combine components written by independent people, teams, and organizations.' + : 'The library for web and native user interfaces'; + return ( + + + {title != null && {pageTitle}} + {isHomePage && ( + // Let Google figure out a good description for each page. + + )} + + + {finishedTranslations.map((languageCode) => ( - {finishedTranslations.map((languageCode) => ( - - ))} - - - - {title != null && ( - - )} - {description != null && ( - - )} + ))} + + + + {title != null && ( + + )} + {description != null && ( + )} + + + + + {title != null && ( + + )} + {description != null && ( - - - {title != null && ( - - )} - {description != null && ( - - )} - - - {searchOrder != null && ( - - )} - - - - - - - - - - {children} - - ); - } -); + )} + + + {searchOrder != null && ( + + )} + + + + + + + + + + {children} + + ); +} diff --git a/src/components/ErrorDecoderContext.tsx b/src/components/_/ErrorDecoderContext.tsx similarity index 92% rename from src/components/ErrorDecoderContext.tsx rename to src/components/_/ErrorDecoderContext.tsx index 77e9ebf7d5b..c4b209a058a 100644 --- a/src/components/ErrorDecoderContext.tsx +++ b/src/components/_/ErrorDecoderContext.tsx @@ -1,3 +1,5 @@ +'use client'; + /** * Copyright (c) Meta Platforms, Inc. and affiliates. * @@ -19,6 +21,8 @@ export const ErrorDecoderContext = createContext< | typeof notInErrorDecoderContext >(notInErrorDecoderContext); +export const ErrorDecoderProvider = ErrorDecoderContext.Provider; + export const useErrorDecoderParams = () => { const params = useContext(ErrorDecoderContext); diff --git a/src/components/_/README.md b/src/components/_/README.md new file mode 100644 index 00000000000..8c5baae4928 --- /dev/null +++ b/src/components/_/README.md @@ -0,0 +1,7 @@ +# `components/_` folder {/*components_-folder*/} + +This folder surves as a temporary location during transition from Next.js Pages Router to Next.js App Router. During this phase, many layout components may be shared bwetween both Next.js Pages Router and Next.js App Router. + +Due to the requirements of the Next.js Pages Router, any components under this foldeer must either be shared components or client components. React Server Components are not allowed in this folder. + +Once the migration to Next.js App Router is complete, this folder will be removed, and all components will be relocated to their appropriate locations. diff --git a/src/components/_/root-layout.tsx b/src/components/_/root-layout.tsx new file mode 100644 index 00000000000..6473e815b26 --- /dev/null +++ b/src/components/_/root-layout.tsx @@ -0,0 +1,154 @@ +import type React from 'react'; + +export function SharedRootHead() { + return ( + <> + + + + + + + +