Skip to content

Commit

Permalink
Unwrap promise with experimental_use (#40575)
Browse files Browse the repository at this point in the history
x-ref: facebook/react#25267

Bump the version of `react-server-dom-webpack` and use `experimental_use` to unwrap the promise to access RSC payload instead of using `readRoot`. `readRoot` is removed from the response type.
  • Loading branch information
huozhi committed Sep 15, 2022
1 parent 33a6dca commit 40b2d13
Show file tree
Hide file tree
Showing 11 changed files with 428 additions and 194 deletions.
5 changes: 4 additions & 1 deletion packages/next/client/app-index.tsx
Expand Up @@ -14,6 +14,9 @@ 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 @@ -143,7 +146,7 @@ function ServerRoot({ cacheKey }: { cacheKey: string }) {
rscCache.delete(cacheKey)
})
const response = useInitialServerResponse(cacheKey)
const root = response.readRoot()
const root = use(response)
return root
}

Expand Down
25 changes: 10 additions & 15 deletions packages/next/client/components/app-router.client.tsx
Expand Up @@ -65,8 +65,8 @@ export function fetchServerResponse(
url: URL,
flightRouterState: FlightRouterState,
prefetch?: true
): { readRoot: () => FlightData } {
// Handle the `fetch` readable stream that can be read using `readRoot`.
): Promise<FlightData> {
// Handle the `fetch` readable stream that can be unwrapped by `React.use`.
return createFromReadableStream(fetchFlight(url, flightRouterState, prefetch))
}

Expand Down Expand Up @@ -212,20 +212,15 @@ export default function AppRouter({
window.history.state?.tree || initialTree,
true
)
try {
r.readRoot()
} catch (e) {
await e
const flightData = r.readRoot()
// @ts-ignore startTransition exists
React.startTransition(() => {
dispatch({
type: ACTION_PREFETCH,
url,
flightData,
})
const flightData = await r
// @ts-ignore startTransition exists
React.startTransition(() => {
dispatch({
type: ACTION_PREFETCH,
url,
flightData,
})
}
})
} catch (err) {
console.error('PREFETCH ERROR', err)
}
Expand Down
9 changes: 6 additions & 3 deletions packages/next/client/components/layout-router.client.tsx
Expand Up @@ -17,6 +17,9 @@ 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 @@ -218,14 +221,14 @@ export function InnerLayoutRouter({
throw new Error('Child node should not have both subTreeData and data')
}

// If cache node has a data request we have to readRoot and update the cache.
// If cache node has a data request we have to unwrap response by `use` and update the cache.
if (childNode.data) {
// TODO-APP: error case
/**
* Flight response data
*/
// When the data has not resolved yet readRoot will suspend here.
const flightData = childNode.data.readRoot()
// When the data has not resolved yet `use` will suspend here.
const flightData = use(childNode.data)

// Handle case when navigating to page in `pages` from `app`
if (typeof flightData === 'string') {
Expand Down
13 changes: 8 additions & 5 deletions packages/next/client/components/reducer.ts
Expand Up @@ -6,9 +6,13 @@ import type {
FlightSegmentPath,
Segment,
} from '../../server/app-render'
import React 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 @@ -730,8 +734,7 @@ export function reducer(
cache,
state.cache,
segments.slice(1),
(): { readRoot: () => FlightData } =>
fetchServerResponse(url, optimisticTree)
(): Promise<FlightData> => fetchServerResponse(url, optimisticTree)
)

// If optimistic fetch couldn't happen it falls back to the non-optimistic case.
Expand Down Expand Up @@ -761,8 +764,8 @@ export function reducer(
cache.data = fetchServerResponse(url, state.tree)
}

// readRoot to suspend here (in the reducer) until the fetch resolves.
const flightData = cache.data.readRoot()
// Unwrap cache data with `use` to suspend here (in the reducer) until the fetch resolves.
const flightData = use(cache.data)

// Handle case when navigating to page in `pages` from `app`
if (typeof flightData === 'string') {
Expand Down Expand Up @@ -953,7 +956,7 @@ export function reducer(
'refetch',
])
}
const flightData = cache.data.readRoot()
const flightData = use(cache.data)

// Handle case when navigating to page in `pages` from `app`
if (typeof flightData === 'string') {
Expand Down
Expand Up @@ -162,6 +162,7 @@ function processErrorChunk(request, id, message, stack) {
return stringToChunk(row);
}
function processModelChunk(request, id, model) {
// $FlowFixMe: `json` might be `undefined` when model is a symbol.
var json = stringify(model, request.toJSON);
var row = serializeRowHeader('J', id) + json + '\n';
return stringToChunk(row);
Expand All @@ -172,6 +173,7 @@ function processReferenceChunk(request, id, reference) {
return stringToChunk(row);
}
function processModuleChunk(request, id, moduleMetaData) {
// $FlowFixMe: `json` might be `undefined` when moduleMetaData is a symbol.
var json = stringify(moduleMetaData);
var row = serializeRowHeader('M', id) + json + '\n';
return stringToChunk(row);
Expand Down Expand Up @@ -894,8 +896,8 @@ function readContext(context) {
return value;
}

// Corresponds to ReactFiberWakeable module. Generally, changes to one module
// should be reflected in the other.
// Corresponds to ReactFiberWakeable and ReactFizzWakeable modules. Generally,
// changes to one module should be reflected in the others.
// TODO: Rename this module and the corresponding Fiber one to "Thenable"
// instead of "Wakeable". Or some other more appropriate name.
// TODO: Sparse arrays are bad for performance.
Expand All @@ -917,11 +919,6 @@ function trackSuspendedWakeable(wakeable) {
// a listener that will update its status and result when it resolves.

switch (thenable.status) {
case 'pending':
// Since the status is already "pending", we can assume it will be updated
// when it resolves, either by React or something in userspace.
break;

case 'fulfilled':
case 'rejected':
// A thenable that already resolved shouldn't have been thrown, so this is
Expand All @@ -933,9 +930,13 @@ function trackSuspendedWakeable(wakeable) {

default:
{
// TODO: Only instrument the thenable if the status if not defined. If
// it's defined, but an unknown value, assume it's been instrumented by
// some custom userspace implementation.
if (typeof thenable.status === 'string') {
// Only instrument the thenable if the status if not defined. If
// it's defined, but an unknown value, assume it's been instrumented by
// some custom userspace implementation. We treat it as "pending".
break;
}

var pendingThenable = thenable;
pendingThenable.status = 'pending';
pendingThenable.then(function (fulfilledValue) {
Expand Down Expand Up @@ -990,7 +991,9 @@ function prepareToUseHooksForComponent(prevThenableState) {
thenableState = prevThenableState;
}
function getThenableStateAfterSuspending() {
return thenableState;
var state = thenableState;
thenableState = null;
return state;
}

function readContext$1(context) {
Expand Down Expand Up @@ -1149,6 +1152,9 @@ function use(usable) {
}
}
}
} else if (usable.$$typeof === REACT_SERVER_CONTEXT_TYPE) {
var context = usable;
return readContext$1(context);
}
} // eslint-disable-next-line react-internal/safe-string-coercion

Expand All @@ -1159,7 +1165,8 @@ function use(usable) {
var ContextRegistry = ReactSharedInternals.ContextRegistry;
function getOrCreateServerContext(globalName) {
if (!ContextRegistry[globalName]) {
ContextRegistry[globalName] = React.createServerContext(globalName, REACT_SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED);
ContextRegistry[globalName] = React.createServerContext(globalName, // $FlowFixMe function signature doesn't reflect the symbol value
REACT_SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED);
}

return ContextRegistry[globalName];
Expand Down Expand Up @@ -1292,7 +1299,8 @@ function attemptResolveElement(type, key, ref, props, prevThenableState) {
if (extraKeys.length !== 0) {
error('ServerContext can only have a value prop and children. Found: %s', JSON.stringify(extraKeys));
}
}
} // $FlowFixMe issue discovered when updating Flow


return [REACT_ELEMENT_TYPE, type, key, // Rely on __popProvider being serialized last to pop the provider.
{
Expand Down Expand Up @@ -1677,7 +1685,8 @@ function resolveModelToJSON(request, parent, key, value) {
}
}
}
}
} // $FlowFixMe


return value;
}
Expand Down Expand Up @@ -1708,12 +1717,14 @@ function resolveModelToJSON(request, parent, key, value) {

if (existingId !== undefined) {
return serializeByValueID(existingId);
}
} // $FlowFixMe `description` might be undefined


var name = value.description;
var name = value.description; // $FlowFixMe `name` might be undefined

if (Symbol.for(name) !== value) {
throw new Error('Only global symbols received from Symbol.for(...) can be passed to client components. ' + ("The symbol Symbol.for(" + value.description + ") cannot be found among global symbols. ") + ("Remove " + describeKeyForErrorMessage(key) + " from this object, or avoid the entire object: " + describeObjectForErrorMessage(parent)));
throw new Error('Only global symbols received from Symbol.for(...) can be passed to client components. ' + ("The symbol Symbol.for(" + // $FlowFixMe `description` might be undefined
value.description + ") cannot be found among global symbols. ") + ("Remove " + describeKeyForErrorMessage(key) + " from this object, or avoid the entire object: " + describeObjectForErrorMessage(parent)));
}

request.pendingChunks++;
Expand Down Expand Up @@ -1772,6 +1783,7 @@ function emitErrorChunk(request, id, error) {
}

function emitModuleChunk(request, id, moduleMetaData) {
// $FlowFixMe ModuleMetaData is not a ReactModel
var processedChunk = processModuleChunk(request, id, moduleMetaData);
request.completedModuleChunks.push(processedChunk);
}
Expand Down

0 comments on commit 40b2d13

Please sign in to comment.