Skip to content

Commit 528fb6d

Browse files
sebmarkbageAndyPengc12
authored andcommitted
[Flight] Unify plain Server Component and forwardRef under one function (facebook#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.
1 parent 2e4e662 commit 528fb6d

File tree

1 file changed

+54
-75
lines changed

1 file changed

+54
-75
lines changed

packages/react-server/src/ReactFlightServer.js

Lines changed: 54 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,58 @@ function createLazyWrapperAroundWakeable(wakeable: Wakeable) {
478478
return lazyType;
479479
}
480480

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+
481533
function renderFragment(
482534
request: Request,
483535
task: Task,
@@ -581,48 +633,7 @@ function renderElement(
581633
return renderClientElement(task, type, key, props);
582634
}
583635
// 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);
626637
} else if (typeof type === 'string') {
627638
// This is a host element. E.g. HTML.
628639
return renderClientElement(task, type, key, props);
@@ -660,39 +671,7 @@ function renderElement(
660671
return renderElement(request, task, wrappedType, key, ref, props);
661672
}
662673
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);
696675
}
697676
case REACT_MEMO_TYPE: {
698677
return renderElement(request, task, type.type, key, ref, props);

0 commit comments

Comments
 (0)