Refactor fetchServerResponse (#40674)

This commit is contained in:
Tim Neutkens 2022-09-19 14:17:20 +02:00 committed by GitHub
parent 356b6cec43
commit c7e2619313
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 43 additions and 39 deletions

View file

@ -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<JSX.Element> {
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 <ServerRoot {...props} cacheKey={cacheKey} />
}

View file

@ -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<FlightData> {
): 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({

View file

@ -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') {

View file

@ -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<typeof fetchServerResponse>
): { bailOptimistic: boolean } | undefined {
const isLastEntry = segments.length === 1
@ -734,7 +732,7 @@ export function reducer(
cache,
state.cache,
segments.slice(1),
(): Promise<FlightData> => 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') {

View file

@ -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<JSX.Element> | 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<Uint8Array>,
req: ReadableStream<Uint8Array>,
serverComponentManifest: any,
flightResponseRef: {
current: ReturnType<typeof createFromReadableStream> | null
},
flightResponseRef: FlightResponseRef,
nonce?: string
) {
if (flightResponseRef.current) {
): Promise<JSX.Element> {
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,

View file

@ -39,6 +39,9 @@ declare module 'react' {
interface LinkHTMLAttributes<T> extends HTMLAttributes<T> {
nonce?: string
}
// TODO-APP: check if this is the right type.
function experimental_use<T>(promise: Promise<T>): Awaited<T>
}
export type Redirect =