Skip to content

Commit

Permalink
Refactor fetchServerResponse (#40674)
Browse files Browse the repository at this point in the history
  • Loading branch information
timneutkens committed Sep 19, 2022
1 parent 356b6ce commit c7e2619
Show file tree
Hide file tree
Showing 6 changed files with 43 additions and 39 deletions.
12 changes: 5 additions & 7 deletions packages/next/client/app-index.tsx
Expand Up @@ -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'
Expand All @@ -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 = {}
Expand Down Expand Up @@ -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

Expand All @@ -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)
})
Expand Down Expand Up @@ -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} />
}
Expand Down
11 changes: 6 additions & 5 deletions packages/next/client/components/app-router.client.tsx
Expand Up @@ -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
Expand All @@ -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]
}

/**
Expand Down Expand Up @@ -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({
Expand Down
13 changes: 8 additions & 5 deletions 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
Expand All @@ -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
*/
Expand Down Expand Up @@ -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') {
Expand Down
14 changes: 6 additions & 8 deletions packages/next/client/components/reducer.ts
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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') {
Expand Down
29 changes: 15 additions & 14 deletions packages/next/server/app-render.tsx
Expand Up @@ -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'
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -197,7 +198,7 @@ function useFlightResponse(
}
process()

return flightResponseRef.current
return res
}

/**
Expand All @@ -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__) {
Expand All @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions packages/next/types/index.d.ts
Expand Up @@ -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 =
Expand Down

0 comments on commit c7e2619

Please sign in to comment.