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.
Sebastian Markbåge committed
Oct 30, 2020 at 16:02 UTC
930ce7c15a50a8d67f24a0bd85ec0937bc84afd6
2 files changed
+57
-24
packages/react-client/src/ReactFlightClient.js
+39
-18
@@ -318,6 +318,17 @@ function createLazyBlock<Props, Data>(
318
return lazyType;
319
}
320
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
+
332
function getChunk(response: Response, id: number): SomeChunk<any> {
333
const chunks = response._chunks;
334
let chunk = chunks.get(id);
@@ -333,26 +344,36 @@ export function parseModelString(
344
parentObject: Object,
345
value: string,
346
): 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);
375
}
349
- // For anything else we must Suspend this block if
350
- // we don't yet have the value.
351
- return readChunk(chunk);
376
}
353
- }
354
- if (value === '@') {
355
- return REACT_BLOCK_TYPE;
377
}
378
return value;
379
}
packages/react-server/src/ReactFlightServer.js
+18
-6
@@ -195,10 +195,14 @@ function createSegment(request: Request, query: () => ReactModel): Segment {
195
return segment;
196
}
197
198
-function serializeIDRef(id: number): string {
198
+function serializeByValueID(id: number): string {
199
return '$' + id.toString(16);
200
}
201
202
+function serializeByRefID(id: number): string {
203
+ return '@' + id.toString(16);
204
+}
205
+
206
function escapeStringValue(value: string): string {
207
if (value[0] === '$' || value[0] === '@') {
208
// We need to escape $ or @ prefixed strings since we use those to encode
@@ -419,13 +423,13 @@ export function resolveModelToJSON(
423
const newSegment = createSegment(request, load);
424
const ping = newSegment.ping;
425
x.then(ping, ping);
422
- return serializeIDRef(newSegment.id);
426
+ return serializeByValueID(newSegment.id);
427
} else {
428
// This load failed, encode the error as a separate row and reference that.
429
request.pendingChunks++;
430
const errorId = request.nextChunkId++;
431
emitErrorChunk(request, errorId, x);
428
- return serializeIDRef(errorId);
432
+ return serializeByValueID(errorId);
433
}
434
}
435
}
@@ -456,7 +460,7 @@ export function resolveModelToJSON(
460
const newSegment = createSegment(request, () => value);
461
const ping = newSegment.ping;
462
x.then(ping, ping);
459
- return serializeIDRef(newSegment.id);
463
+ return serializeByValueID(newSegment.id);
464
} else {
465
// Something errored. Don't bother encoding anything up to here.
466
throw x;
@@ -479,12 +483,20 @@ export function resolveModelToJSON(
483
request.pendingChunks++;
484
const moduleId = request.nextChunkId++;
485
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);
495
} catch (x) {
496
request.pendingChunks++;
497
const errorId = request.nextChunkId++;
498
emitErrorChunk(request, errorId, x);
487
- return serializeIDRef(errorId);
499
+ return serializeByValueID(errorId);
500
}
501
}
502