diff --git a/packages/next/client/app-index.tsx b/packages/next/client/app-index.tsx index 552b2c00f739887..c8ede9eaa67dac4 100644 --- a/packages/next/client/app-index.tsx +++ b/packages/next/client/app-index.tsx @@ -2,7 +2,8 @@ import '../build/polyfills/polyfill-module' // @ts-ignore react-dom/client exists when using React 18 import ReactDOMClient from 'react-dom/client' -import React from 'react' +// TODO-APP: change to React.use once it becomes stable +import React, { experimental_use as use } from 'react' import { createFromReadableStream } from 'next/dist/compiled/react-server-dom-webpack' import measureWebVitals from './performance-relayer' @@ -16,9 +17,6 @@ declare global { const __webpack_require__: any } -// TODO-APP: change to React.use once it becomes stable -const use = (React as any).experimental_use - // eslint-disable-next-line no-undef const getChunkScriptFilename = __webpack_require__.u const chunkFilenameMap: any = {} @@ -127,7 +125,7 @@ function createResponseCache() { } const rscCache = createResponseCache() -function useInitialServerResponse(cacheKey: string) { +function useInitialServerResponse(cacheKey: string): Promise { const response = rscCache.get(cacheKey) if (response) return response @@ -143,7 +141,7 @@ function useInitialServerResponse(cacheKey: string) { return newResponse } -function ServerRoot({ cacheKey }: { cacheKey: string }) { +function ServerRoot({ cacheKey }: { cacheKey: string }): JSX.Element { React.useEffect(() => { rscCache.delete(cacheKey) }) @@ -171,7 +169,7 @@ function Root({ children }: React.PropsWithChildren<{}>): React.ReactElement { return children as React.ReactElement } -function RSCComponent(props: any) { +function RSCComponent(props: any): JSX.Element { const cacheKey = getCacheKey() return } diff --git a/packages/next/client/components/app-router.client.tsx b/packages/next/client/components/app-router.client.tsx index e2950f45588eab4..9bd71e4376d6d48 100644 --- a/packages/next/client/components/app-router.client.tsx +++ b/packages/next/client/components/app-router.client.tsx @@ -32,11 +32,11 @@ import { useReducerWithReduxDevtools } from './use-reducer-with-devtools' /** * Fetch the flight data for the provided url. Takes in the current router state to decide what to render server-side. */ -export function fetchServerResponse( +export async function fetchServerResponse( url: URL, flightRouterState: FlightRouterState, prefetch?: true -): Promise { +): Promise<[FlightData: FlightData]> { const flightUrl = new URL(url) const searchParams = flightUrl.searchParams // Enable flight response @@ -50,9 +50,10 @@ export function fetchServerResponse( searchParams.append('__flight_prefetch__', '1') } - const promise = fetch(flightUrl.toString()) + const res = await fetch(flightUrl.toString()) // Handle the `fetch` readable stream that can be unwrapped by `React.use`. - return createFromFetch(promise) + const flightData: FlightData = await createFromFetch(Promise.resolve(res)) + return [flightData] } /** @@ -197,7 +198,7 @@ export default function AppRouter({ window.history.state?.tree || initialTree, true ) - const flightData = await r + const [flightData] = await r // @ts-ignore startTransition exists React.startTransition(() => { dispatch({ diff --git a/packages/next/client/components/layout-router.client.tsx b/packages/next/client/components/layout-router.client.tsx index 5dfa3fb80fd4b58..8de34509af60f84 100644 --- a/packages/next/client/components/layout-router.client.tsx +++ b/packages/next/client/components/layout-router.client.tsx @@ -1,6 +1,12 @@ 'client' -import React, { useContext, useEffect, useRef } from 'react' +import React, { + useContext, + useEffect, + useRef, + // TODO-APP: change to React.use once it becomes stable + experimental_use as use, +} from 'react' import type { ChildProp, //Segment @@ -19,9 +25,6 @@ import { import { fetchServerResponse } from './app-router.client' // import { matchSegment } from './match-segments' -// TODO-APP: change to React.use once it becomes stable -const use = (React as any).experimental_use - /** * Check if every segment in array a and b matches */ @@ -230,7 +233,7 @@ export function InnerLayoutRouter({ * Flight response data */ // When the data has not resolved yet `use` will suspend here. - const flightData = use(childNode.data) + const [flightData] = use(childNode.data) // Handle case when navigating to page in `pages` from `app` if (typeof flightData === 'string') { diff --git a/packages/next/client/components/reducer.ts b/packages/next/client/components/reducer.ts index d8c6cb2c3bec889..60c33a9916938cf 100644 --- a/packages/next/client/components/reducer.ts +++ b/packages/next/client/components/reducer.ts @@ -6,13 +6,11 @@ import type { FlightSegmentPath, Segment, } from '../../server/app-render' -import React from 'react' +// TODO-APP: change to React.use once it becomes stable +import { experimental_use as use } from 'react' import { matchSegment } from './match-segments' import { fetchServerResponse } from './app-router.client' -// TODO-APP: change to React.use once it becomes stable -const use = (React as any).experimental_use - /** * Invalidate cache one level down from the router state. */ @@ -233,7 +231,7 @@ function fillCacheWithDataProperty( newCache: CacheNode, existingCache: CacheNode, segments: string[], - fetchResponse: any + fetchResponse: () => ReturnType ): { bailOptimistic: boolean } | undefined { const isLastEntry = segments.length === 1 @@ -734,7 +732,7 @@ export function reducer( cache, state.cache, segments.slice(1), - (): Promise => fetchServerResponse(url, optimisticTree) + () => fetchServerResponse(url, optimisticTree) ) // If optimistic fetch couldn't happen it falls back to the non-optimistic case. @@ -765,7 +763,7 @@ export function reducer( } // Unwrap cache data with `use` to suspend here (in the reducer) until the fetch resolves. - const flightData = use(cache.data) + const [flightData] = use(cache.data) // Handle case when navigating to page in `pages` from `app` if (typeof flightData === 'string') { @@ -956,7 +954,7 @@ export function reducer( 'refetch', ]) } - const flightData = use(cache.data) + const [flightData] = use(cache.data) // Handle case when navigating to page in `pages` from `app` if (typeof flightData === 'string') { diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx index ae12b0335b5b404..f115df782da17ef 100644 --- a/packages/next/server/app-render.tsx +++ b/packages/next/server/app-render.tsx @@ -2,7 +2,8 @@ import type { IncomingHttpHeaders, IncomingMessage, ServerResponse } from 'http' import type { LoadComponentsReturnType } from './load-components' import type { ServerRuntime } from '../types' -import React from 'react' +// TODO-APP: change to React.use once it becomes stable +import React, { experimental_use as use } from 'react' import { ParsedUrlQuery, stringify as stringifyQuery } from 'querystring' import { createFromReadableStream } from 'next/dist/compiled/react-server-dom-webpack' import { renderToReadableStream } from 'next/dist/compiled/react-server-dom-webpack/writer.browser.server' @@ -29,9 +30,6 @@ import { FlushEffectsContext } from '../shared/lib/flush-effects' import { stripInternalQueries } from './internal-utils' import type { ComponentsType } from '../build/webpack/loaders/next-app-loader' -// TODO-APP: change to React.use once it becomes stable -const use = (React as any).experimental_use - // this needs to be required lazily so that `next-server` can set // the env before we require const ReactDOMServer = shouldUseReactRoot @@ -139,6 +137,10 @@ function preloadDataFetchingRecord( return record } +interface FlightResponseRef { + current: Promise | null +} + /** * Render Flight stream. * This is only used for renderToHTML, the Flight response does not need additional wrappers. @@ -147,19 +149,18 @@ function useFlightResponse( writable: WritableStream, req: ReadableStream, serverComponentManifest: any, - flightResponseRef: { - current: ReturnType | null - }, + flightResponseRef: FlightResponseRef, nonce?: string -) { - if (flightResponseRef.current) { +): Promise { + if (flightResponseRef.current !== null) { return flightResponseRef.current } const [renderStream, forwardStream] = readableStreamTee(req) - flightResponseRef.current = createFromReadableStream(renderStream, { + const res = createFromReadableStream(renderStream, { moduleMap: serverComponentManifest.__ssr_module_mapping__, }) + flightResponseRef.current = res let bootstrapped = false // We only attach CSS chunks to the inlined data. @@ -197,7 +198,7 @@ function useFlightResponse( } process() - return flightResponseRef.current + return res } /** @@ -224,7 +225,7 @@ function createServerComponentRenderer( > }, nonce?: string -) { +): () => JSX.Element { // We need to expose the `__webpack_require__` API globally for // react-server-dom-webpack. This is a hack until we find a better way. if (ComponentMod.__next_app_webpack_require__ || ComponentMod.__next_rsc__) { @@ -251,10 +252,10 @@ function createServerComponentRenderer( return RSCStream } - const flightResponseRef = { current: null } + const flightResponseRef: FlightResponseRef = { current: null } const writable = transformStream.writable - return function ServerComponentWrapper() { + return function ServerComponentWrapper(): JSX.Element { const reqStream = createRSCStream() const response = useFlightResponse( writable, diff --git a/packages/next/types/index.d.ts b/packages/next/types/index.d.ts index 25b63fe25533161..28866d91b134c3b 100644 --- a/packages/next/types/index.d.ts +++ b/packages/next/types/index.d.ts @@ -39,6 +39,9 @@ declare module 'react' { interface LinkHTMLAttributes extends HTMLAttributes { nonce?: string } + + // TODO-APP: check if this is the right type. + function experimental_use(promise: Promise): Awaited } export type Redirect =