@samitouri / QOS-React-2 / commits / 930ce7c15a

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