From 930ce7c15a50a8d67f24a0bd85ec0937bc84afd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Fri, 30 Oct 2020 16:02:03 -0400 Subject: [PATCH] Allow values to be encoded by "reference" to a value rather than the value itself (#20136) These references are currently transformed into React.lazy values. We can use these in React positions like element type or node position. This could be expanded to a more general concept like Suspensey Promises, asset references or JSResourceReferences. For now it's only used in React Element type position. The purpose of these is to let you suspend deeper in the tree. --- .../react-client/src/ReactFlightClient.js | 57 +++++++++++++------ .../react-server/src/ReactFlightServer.js | 24 ++++++-- 2 files changed, 57 insertions(+), 24 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 2b5a692b2e..7450e736de 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -318,6 +318,17 @@ function createLazyBlock( return lazyType; } +function createLazyChunkWrapper( + chunk: SomeChunk, +): LazyComponent> { + const lazyType: LazyComponent> = { + $$typeof: REACT_LAZY_TYPE, + _payload: chunk, + _init: readChunk, + }; + return lazyType; +} + function getChunk(response: Response, id: number): SomeChunk { const chunks = response._chunks; let chunk = chunks.get(id); @@ -333,26 +344,36 @@ export function parseModelString( parentObject: Object, value: string, ): any { - if (value[0] === '$') { - if (value === '$') { - return REACT_ELEMENT_TYPE; - } else if (value[1] === '$' || value[1] === '@') { - // This was an escaped string value. - return value.substring(1); - } else { - const id = parseInt(value.substring(1), 16); - const chunk = getChunk(response, id); - if (parentObject[0] === REACT_BLOCK_TYPE) { - // Block types know how to deal with lazy values. - return chunk; + switch (value[0]) { + case '$': { + if (value === '$') { + return REACT_ELEMENT_TYPE; + } else if (value[1] === '$' || value[1] === '@') { + // This was an escaped string value. + return value.substring(1); + } else { + const id = parseInt(value.substring(1), 16); + const chunk = getChunk(response, id); + if (parentObject[0] === REACT_BLOCK_TYPE) { + // Block types know how to deal with lazy values. + return chunk; + } + // For anything else we must Suspend this block if + // we don't yet have the value. + return readChunk(chunk); + } + } + case '@': { + if (value === '@') { + return REACT_BLOCK_TYPE; + } else { + const id = parseInt(value.substring(1), 16); + const chunk = getChunk(response, id); + // We create a React.lazy wrapper around any lazy values. + // When passed into React, we'll know how to suspend on this. + return createLazyChunkWrapper(chunk); } - // For anything else we must Suspend this block if - // we don't yet have the value. - return readChunk(chunk); } - } - if (value === '@') { - return REACT_BLOCK_TYPE; } return value; } diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 51e5d56583..e82171d24d 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -195,10 +195,14 @@ function createSegment(request: Request, query: () => ReactModel): Segment { return segment; } -function serializeIDRef(id: number): string { +function serializeByValueID(id: number): string { return '$' + id.toString(16); } +function serializeByRefID(id: number): string { + return '@' + id.toString(16); +} + function escapeStringValue(value: string): string { if (value[0] === '$' || value[0] === '@') { // We need to escape $ or @ prefixed strings since we use those to encode @@ -419,13 +423,13 @@ export function resolveModelToJSON( const newSegment = createSegment(request, load); const ping = newSegment.ping; x.then(ping, ping); - return serializeIDRef(newSegment.id); + return serializeByValueID(newSegment.id); } else { // This load failed, encode the error as a separate row and reference that. request.pendingChunks++; const errorId = request.nextChunkId++; emitErrorChunk(request, errorId, x); - return serializeIDRef(errorId); + return serializeByValueID(errorId); } } } @@ -456,7 +460,7 @@ export function resolveModelToJSON( const newSegment = createSegment(request, () => value); const ping = newSegment.ping; x.then(ping, ping); - return serializeIDRef(newSegment.id); + return serializeByValueID(newSegment.id); } else { // Something errored. Don't bother encoding anything up to here. throw x; @@ -479,12 +483,20 @@ export function resolveModelToJSON( request.pendingChunks++; const moduleId = request.nextChunkId++; emitModuleChunk(request, moduleId, moduleMetaData); - return serializeIDRef(moduleId); + if (parent[0] === REACT_ELEMENT_TYPE && key === '1') { + // If we're encoding the "type" of an element, we can refer + // to that by a lazy reference instead of directly since React + // knows how to deal with lazy values. This lets us suspend + // on this component rather than its parent until the code has + // loaded. + return serializeByRefID(moduleId); + } + return serializeByValueID(moduleId); } catch (x) { request.pendingChunks++; const errorId = request.nextChunkId++; emitErrorChunk(request, errorId, x); - return serializeIDRef(errorId); + return serializeByValueID(errorId); } }