Skip to content

Commit 930ce7c

Browse files
authored
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.
1 parent 39eb6d1 commit 930ce7c

File tree

2 files changed

+57
-24
lines changed

2 files changed

+57
-24
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,17 @@ function createLazyBlock<Props, Data>(
318318
return lazyType;
319319
}
320320

321+
function createLazyChunkWrapper<T>(
322+
chunk: SomeChunk<T>,
323+
): LazyComponent<T, SomeChunk<T>> {
324+
const lazyType: LazyComponent<T, SomeChunk<T>> = {
325+
$$typeof: REACT_LAZY_TYPE,
326+
_payload: chunk,
327+
_init: readChunk,
328+
};
329+
return lazyType;
330+
}
331+
321332
function getChunk(response: Response, id: number): SomeChunk<any> {
322333
const chunks = response._chunks;
323334
let chunk = chunks.get(id);
@@ -333,26 +344,36 @@ export function parseModelString(
333344
parentObject: Object,
334345
value: string,
335346
): any {
336-
if (value[0] === '$') {
337-
if (value === '$') {
338-
return REACT_ELEMENT_TYPE;
339-
} else if (value[1] === '$' || value[1] === '@') {
340-
// This was an escaped string value.
341-
return value.substring(1);
342-
} else {
343-
const id = parseInt(value.substring(1), 16);
344-
const chunk = getChunk(response, id);
345-
if (parentObject[0] === REACT_BLOCK_TYPE) {
346-
// Block types know how to deal with lazy values.
347-
return chunk;
347+
switch (value[0]) {
348+
case '$': {
349+
if (value === '$') {
350+
return REACT_ELEMENT_TYPE;
351+
} else if (value[1] === '$' || value[1] === '@') {
352+
// This was an escaped string value.
353+
return value.substring(1);
354+
} else {
355+
const id = parseInt(value.substring(1), 16);
356+
const chunk = getChunk(response, id);
357+
if (parentObject[0] === REACT_BLOCK_TYPE) {
358+
// Block types know how to deal with lazy values.
359+
return chunk;
360+
}
361+
// For anything else we must Suspend this block if
362+
// we don't yet have the value.
363+
return readChunk(chunk);
364+
}
365+
}
366+
case '@': {
367+
if (value === '@') {
368+
return REACT_BLOCK_TYPE;
369+
} else {
370+
const id = parseInt(value.substring(1), 16);
371+
const chunk = getChunk(response, id);
372+
// We create a React.lazy wrapper around any lazy values.
373+
// When passed into React, we'll know how to suspend on this.
374+
return createLazyChunkWrapper(chunk);
348375
}
349-
// For anything else we must Suspend this block if
350-
// we don't yet have the value.
351-
return readChunk(chunk);
352376
}
353-
}
354-
if (value === '@') {
355-
return REACT_BLOCK_TYPE;
356377
}
357378
return value;
358379
}

packages/react-server/src/ReactFlightServer.js

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -195,10 +195,14 @@ function createSegment(request: Request, query: () => ReactModel): Segment {
195195
return segment;
196196
}
197197

198-
function serializeIDRef(id: number): string {
198+
function serializeByValueID(id: number): string {
199199
return '$' + id.toString(16);
200200
}
201201

202+
function serializeByRefID(id: number): string {
203+
return '@' + id.toString(16);
204+
}
205+
202206
function escapeStringValue(value: string): string {
203207
if (value[0] === '$' || value[0] === '@') {
204208
// We need to escape $ or @ prefixed strings since we use those to encode
@@ -419,13 +423,13 @@ export function resolveModelToJSON(
419423
const newSegment = createSegment(request, load);
420424
const ping = newSegment.ping;
421425
x.then(ping, ping);
422-
return serializeIDRef(newSegment.id);
426+
return serializeByValueID(newSegment.id);
423427
} else {
424428
// This load failed, encode the error as a separate row and reference that.
425429
request.pendingChunks++;
426430
const errorId = request.nextChunkId++;
427431
emitErrorChunk(request, errorId, x);
428-
return serializeIDRef(errorId);
432+
return serializeByValueID(errorId);
429433
}
430434
}
431435
}
@@ -456,7 +460,7 @@ export function resolveModelToJSON(
456460
const newSegment = createSegment(request, () => value);
457461
const ping = newSegment.ping;
458462
x.then(ping, ping);
459-
return serializeIDRef(newSegment.id);
463+
return serializeByValueID(newSegment.id);
460464
} else {
461465
// Something errored. Don't bother encoding anything up to here.
462466
throw x;
@@ -479,12 +483,20 @@ export function resolveModelToJSON(
479483
request.pendingChunks++;
480484
const moduleId = request.nextChunkId++;
481485
emitModuleChunk(request, moduleId, moduleMetaData);
482-
return serializeIDRef(moduleId);
486+
if (parent[0] === REACT_ELEMENT_TYPE && key === '1') {
487+
// If we're encoding the "type" of an element, we can refer
488+
// to that by a lazy reference instead of directly since React
489+
// knows how to deal with lazy values. This lets us suspend
490+
// on this component rather than its parent until the code has
491+
// loaded.
492+
return serializeByRefID(moduleId);
493+
}
494+
return serializeByValueID(moduleId);
483495
} catch (x) {
484496
request.pendingChunks++;
485497
const errorId = request.nextChunkId++;
486498
emitErrorChunk(request, errorId, x);
487-
return serializeIDRef(errorId);
499+
return serializeByValueID(errorId);
488500
}
489501
}
490502

0 commit comments

Comments
 (0)