@samitouri / QOS-React-1 / commits / f07ac1e268

[Flight] Unify plain Server Component and forwardRef under one function (#28261)

This used to be trivial but it's no longer trivial. In Fizz and Fiber this is split into renderWithHooks and finishFunctionComponent since they also support indeterminate components. Interestingly thanks to this unification we always call functions with an arity of 2 which is a bit weird - with the second argument being undefined in everything except forwardRef and legacy context consumers. This makes Flight makes the same thing but we could also call it with an arity of 1. Since Flight errors early if you try to pass it a ref, and there's no legacy context, the second arg is always undefined. The practical change in this PR is that returning a Promise from a forwardRef now turns it into a lazy. We previously didn't support async forwardRef since it wasn't supported on the client. However, since eventually this will be supported by child-as-a-promise it seems fine to support it.

Sebastian Markbåge committed Feb 6, 2024 at 16:41 UTC f07ac1e2680a26c5b3bf9c651d62c792de71d46d
1 file changed +54 -75
packages/react-server/src/ReactFlightServer.js
+54 -75
@@ -478,6 +478,58 @@ function createLazyWrapperAroundWakeable(wakeable: Wakeable) {
478 return lazyType;
479 }
480
481 +function renderFunctionComponent<Props>(
482 + request: Request,
483 + task: Task,
484 + key: null | string,
485 + Component: (p: Props, arg: void) => any,
486 + props: Props,
487 +): ReactJSONValue {
488 + // Reset the task's thenable state before continuing, so that if a later
489 + // component suspends we can reuse the same task object. If the same
490 + // component suspends again, the thenable state will be restored.
491 + const prevThenableState = task.thenableState;
492 + task.thenableState = null;
493 +
494 + prepareToUseHooksForComponent(prevThenableState);
495 + // The secondArg is always undefined in Server Components since refs error early.
496 + const secondArg = undefined;
497 + let result = Component(props, secondArg);
498 + if (
499 + typeof result === 'object' &&
500 + result !== null &&
501 + typeof result.then === 'function'
502 + ) {
503 + // When the return value is in children position we can resolve it immediately,
504 + // to its value without a wrapper if it's synchronously available.
505 + const thenable: Thenable<any> = result;
506 + if (thenable.status === 'fulfilled') {
507 + return thenable.value;
508 + }
509 + // TODO: Once we accept Promises as children on the client, we can just return
510 + // the thenable here.
511 + result = createLazyWrapperAroundWakeable(result);
512 + }
513 + // Track this element's key on the Server Component on the keyPath context..
514 + const prevKeyPath = task.keyPath;
515 + const prevImplicitSlot = task.implicitSlot;
516 + if (key !== null) {
517 + // Append the key to the path. Technically a null key should really add the child
518 + // index. We don't do that to hold the payload small and implementation simple.
519 + task.keyPath = prevKeyPath === null ? key : prevKeyPath + ',' + key;
520 + } else if (prevKeyPath === null) {
521 + // This sequence of Server Components has no keys. This means that it was rendered
522 + // in a slot that needs to assign an implicit key. Even if children below have
523 + // explicit keys, they should not be used for the outer most key since it might
524 + // collide with other slots in that set.
525 + task.implicitSlot = true;
526 + }
527 + const json = renderModelDestructive(request, task, emptyRoot, '', result);
528 + task.keyPath = prevKeyPath;
529 + task.implicitSlot = prevImplicitSlot;
530 + return json;
531 +}
532 +
533 function renderFragment(
534 request: Request,
535 task: Task,
@@ -581,48 +633,7 @@ function renderElement(
633 return renderClientElement(task, type, key, props);
634 }
635 // This is a server-side component.
584 -
585 - // Reset the task's thenable state before continuing, so that if a later
586 - // component suspends we can reuse the same task object. If the same
587 - // component suspends again, the thenable state will be restored.
588 - const prevThenableState = task.thenableState;
589 - task.thenableState = null;
590 -
591 - prepareToUseHooksForComponent(prevThenableState);
592 - let result = type(props);
593 - if (
594 - typeof result === 'object' &&
595 - result !== null &&
596 - typeof result.then === 'function'
597 - ) {
598 - // When the return value is in children position we can resolve it immediately,
599 - // to its value without a wrapper if it's synchronously available.
600 - const thenable: Thenable<any> = result;
601 - if (thenable.status === 'fulfilled') {
602 - return thenable.value;
603 - }
604 - // TODO: Once we accept Promises as children on the client, we can just return
605 - // the thenable here.
606 - result = createLazyWrapperAroundWakeable(result);
607 - }
608 - // Track this element's key on the Server Component on the keyPath context..
609 - const prevKeyPath = task.keyPath;
610 - const prevImplicitSlot = task.implicitSlot;
611 - if (key !== null) {
612 - // Append the key to the path. Technically a null key should really add the child
613 - // index. We don't do that to hold the payload small and implementation simple.
614 - task.keyPath = prevKeyPath === null ? key : prevKeyPath + ',' + key;
615 - } else if (prevKeyPath === null) {
616 - // This sequence of Server Components has no keys. This means that it was rendered
617 - // in a slot that needs to assign an implicit key. Even if children below have
618 - // explicit keys, they should not be used for the outer most key since it might
619 - // collide with other slots in that set.
620 - task.implicitSlot = true;
621 - }
622 - const json = renderModelDestructive(request, task, emptyRoot, '', result);
623 - task.keyPath = prevKeyPath;
624 - task.implicitSlot = prevImplicitSlot;
625 - return json;
636 + return renderFunctionComponent(request, task, key, type, props);
637 } else if (typeof type === 'string') {
638 // This is a host element. E.g. HTML.
639 return renderClientElement(task, type, key, props);
@@ -660,39 +671,7 @@ function renderElement(
671 return renderElement(request, task, wrappedType, key, ref, props);
672 }
673 case REACT_FORWARD_REF_TYPE: {
663 - const render = type.render;
664 -
665 - // Reset the task's thenable state before continuing, so that if a later
666 - // component suspends we can reuse the same task object. If the same
667 - // component suspends again, the thenable state will be restored.
668 - const prevThenableState = task.thenableState;
669 - task.thenableState = null;
670 -
671 - prepareToUseHooksForComponent(prevThenableState);
672 - const result = render(props, undefined);
673 - const prevKeyPath = task.keyPath;
674 - const prevImplicitSlot = task.implicitSlot;
675 - if (key !== null) {
676 - // Append the key to the path. Technically a null key should really add the child
677 - // index. We don't do that to hold the payload small and implementation simple.
678 - task.keyPath = prevKeyPath === null ? key : prevKeyPath + ',' + key;
679 - } else if (prevKeyPath === null) {
680 - // This sequence of Server Components has no keys. This means that it was rendered
681 - // in a slot that needs to assign an implicit key. Even if children below have
682 - // explicit keys, they should not be used for the outer most key since it might
683 - // collide with other slots in that set.
684 - task.implicitSlot = true;
685 - }
686 - const json = renderModelDestructive(
687 - request,
688 - task,
689 - emptyRoot,
690 - '',
691 - result,
692 - );
693 - task.keyPath = prevKeyPath;
694 - task.implicitSlot = prevImplicitSlot;
695 - return json;
674 + return renderFunctionComponent(request, task, key, type.render, props);
675 }
676 case REACT_MEMO_TYPE: {
677 return renderElement(request, task, type.type, key, ref, props);