Skip to content

Commit

Permalink
[Flight] Unify plain Server Component and forwardRef under one functi…
Browse files Browse the repository at this point in the history
…on (#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.

DiffTrain build for [f07ac1e](f07ac1e)
  • Loading branch information
sebmarkbage committed Feb 7, 2024
1 parent de5b760 commit 7a01513
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 137 deletions.
2 changes: 1 addition & 1 deletion compiled/facebook-www/REVISION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1beb94133a93a433669a893aef02dd5afec07394
f07ac1e2680a26c5b3bf9c651d62c792de71d46d
6 changes: 3 additions & 3 deletions compiled/facebook-www/ReactDOMTesting-prod.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -17067,7 +17067,7 @@ Internals.Events = [
var devToolsConfig$jscomp$inline_1787 = {
findFiberByHostInstance: getClosestInstanceFromNode,
bundleType: 0,
version: "18.3.0-www-modern-941395b7",
version: "18.3.0-www-modern-55799591",
rendererPackageName: "react-dom"
};
var internals$jscomp$inline_2160 = {
Expand Down Expand Up @@ -17098,7 +17098,7 @@ var internals$jscomp$inline_2160 = {
scheduleRoot: null,
setRefreshHandler: null,
getCurrentFiber: null,
reconcilerVersion: "18.3.0-www-modern-941395b7"
reconcilerVersion: "18.3.0-www-modern-55799591"
};
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
var hook$jscomp$inline_2161 = __REACT_DEVTOOLS_GLOBAL_HOOK__;
Expand Down Expand Up @@ -17526,4 +17526,4 @@ exports.useFormStatus = function () {
return ReactCurrentDispatcher$2.current.useHostTransitionStatus();
throw Error(formatProdErrorMessage(248));
};
exports.version = "18.3.0-www-modern-941395b7";
exports.version = "18.3.0-www-modern-55799591";
141 changes: 58 additions & 83 deletions compiled/facebook-www/ReactFlightDOMServer-dev.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -1308,6 +1308,55 @@ if (__DEV__) {
return lazyType;
}

function renderFunctionComponent(request, task, key, Component, props) {
// Reset the task's thenable state before continuing, so that if a later
// component suspends we can reuse the same task object. If the same
// component suspends again, the thenable state will be restored.
var prevThenableState = task.thenableState;
task.thenableState = null;
prepareToUseHooksForComponent(prevThenableState); // The secondArg is always undefined in Server Components since refs error early.

var secondArg = undefined;
var result = Component(props, secondArg);

if (
typeof result === "object" &&
result !== null &&
typeof result.then === "function"
) {
// When the return value is in children position we can resolve it immediately,
// to its value without a wrapper if it's synchronously available.
var thenable = result;

if (thenable.status === "fulfilled") {
return thenable.value;
} // TODO: Once we accept Promises as children on the client, we can just return
// the thenable here.

result = createLazyWrapperAroundWakeable(result);
} // Track this element's key on the Server Component on the keyPath context..

var prevKeyPath = task.keyPath;
var prevImplicitSlot = task.implicitSlot;

if (key !== null) {
// Append the key to the path. Technically a null key should really add the child
// index. We don't do that to hold the payload small and implementation simple.
task.keyPath = prevKeyPath === null ? key : prevKeyPath + "," + key;
} else if (prevKeyPath === null) {
// This sequence of Server Components has no keys. This means that it was rendered
// in a slot that needs to assign an implicit key. Even if children below have
// explicit keys, they should not be used for the outer most key since it might
// collide with other slots in that set.
task.implicitSlot = true;
}

var json = renderModelDestructive(request, task, emptyRoot, "", result);
task.keyPath = prevKeyPath;
task.implicitSlot = prevImplicitSlot;
return json;
}

function renderFragment(request, task, children) {
if (task.keyPath !== null) {
// We have a Server Component that specifies a key but we're now splitting
Expand Down Expand Up @@ -1397,74 +1446,30 @@ if (__DEV__) {
// This is a reference to a Client Component.
return renderClientElement(task, type, key, props);
} // This is a server-side component.
// Reset the task's thenable state before continuing, so that if a later
// component suspends we can reuse the same task object. If the same
// component suspends again, the thenable state will be restored.

var prevThenableState = task.thenableState;
task.thenableState = null;
prepareToUseHooksForComponent(prevThenableState);
var result = type(props);

if (
typeof result === "object" &&
result !== null &&
typeof result.then === "function"
) {
// When the return value is in children position we can resolve it immediately,
// to its value without a wrapper if it's synchronously available.
var thenable = result;

if (thenable.status === "fulfilled") {
return thenable.value;
} // TODO: Once we accept Promises as children on the client, we can just return
// the thenable here.

result = createLazyWrapperAroundWakeable(result);
} // Track this element's key on the Server Component on the keyPath context..

var prevKeyPath = task.keyPath;
var prevImplicitSlot = task.implicitSlot;

if (key !== null) {
// Append the key to the path. Technically a null key should really add the child
// index. We don't do that to hold the payload small and implementation simple.
task.keyPath = prevKeyPath === null ? key : prevKeyPath + "," + key;
} else if (prevKeyPath === null) {
// This sequence of Server Components has no keys. This means that it was rendered
// in a slot that needs to assign an implicit key. Even if children below have
// explicit keys, they should not be used for the outer most key since it might
// collide with other slots in that set.
task.implicitSlot = true;
}

var json = renderModelDestructive(request, task, emptyRoot, "", result);
task.keyPath = prevKeyPath;
task.implicitSlot = prevImplicitSlot;
return json;
return renderFunctionComponent(request, task, key, type, props);
} else if (typeof type === "string") {
// This is a host element. E.g. HTML.
return renderClientElement(task, type, key, props);
} else if (typeof type === "symbol") {
if (type === REACT_FRAGMENT_TYPE && key === null) {
// For key-less fragments, we add a small optimization to avoid serializing
// it as a wrapper.
var _prevImplicitSlot = task.implicitSlot;
var prevImplicitSlot = task.implicitSlot;

if (task.keyPath === null) {
task.implicitSlot = true;
}

var _json = renderModelDestructive(
var json = renderModelDestructive(
request,
task,
emptyRoot,
"",
props.children
);

task.implicitSlot = _prevImplicitSlot;
return _json;
task.implicitSlot = prevImplicitSlot;
return json;
} // This might be a built-in React component. We'll let the client decide.
// Any built-in works as long as its props are serializable.

Expand All @@ -1484,43 +1489,13 @@ if (__DEV__) {
}

case REACT_FORWARD_REF_TYPE: {
var render = type.render; // Reset the task's thenable state before continuing, so that if a later
// component suspends we can reuse the same task object. If the same
// component suspends again, the thenable state will be restored.

var _prevThenableState = task.thenableState;
task.thenableState = null;
prepareToUseHooksForComponent(_prevThenableState);

var _result = render(props, undefined);

var _prevKeyPath = task.keyPath;
var _prevImplicitSlot2 = task.implicitSlot;

if (key !== null) {
// Append the key to the path. Technically a null key should really add the child
// index. We don't do that to hold the payload small and implementation simple.
task.keyPath =
_prevKeyPath === null ? key : _prevKeyPath + "," + key;
} else if (_prevKeyPath === null) {
// This sequence of Server Components has no keys. This means that it was rendered
// in a slot that needs to assign an implicit key. Even if children below have
// explicit keys, they should not be used for the outer most key since it might
// collide with other slots in that set.
task.implicitSlot = true;
}

var _json2 = renderModelDestructive(
return renderFunctionComponent(
request,
task,
emptyRoot,
"",
_result
key,
type.render,
props
);

task.keyPath = _prevKeyPath;
task.implicitSlot = _prevImplicitSlot2;
return _json2;
}

case REACT_MEMO_TYPE: {
Expand Down
84 changes: 35 additions & 49 deletions compiled/facebook-www/ReactFlightDOMServer-prod.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,31 @@ function createLazyWrapperAroundWakeable(wakeable) {
}
return { $$typeof: REACT_LAZY_TYPE, _payload: wakeable, _init: readThenable };
}
function renderFunctionComponent(request, task, key, Component, props) {
var prevThenableState = task.thenableState;
task.thenableState = null;
thenableIndexCounter = 0;
thenableState = prevThenableState;
Component = Component(props, void 0);
if (
"object" === typeof Component &&
null !== Component &&
"function" === typeof Component.then
) {
props = Component;
if ("fulfilled" === props.status) return props.value;
Component = createLazyWrapperAroundWakeable(Component);
}
props = task.keyPath;
prevThenableState = task.implicitSlot;
null !== key
? (task.keyPath = null === props ? key : props + "," + key)
: null === props && (task.implicitSlot = !0);
request = renderModelDestructive(request, task, emptyRoot, "", Component);
task.keyPath = props;
task.implicitSlot = prevThenableState;
return request;
}
function renderFragment(request, task, children) {
return null !== task.keyPath
? ((request = [
Expand All @@ -563,33 +588,10 @@ function renderElement(request, task, type, key, ref, props) {
throw Error(
"Refs cannot be used in Server Components, nor passed to Client Components."
);
if ("function" === typeof type) {
if (isClientReference(type))
return renderClientElement(task, type, key, props);
ref = task.thenableState;
task.thenableState = null;
thenableIndexCounter = 0;
thenableState = ref;
props = type(props);
if (
"object" === typeof props &&
null !== props &&
"function" === typeof props.then
) {
type = props;
if ("fulfilled" === type.status) return type.value;
props = createLazyWrapperAroundWakeable(props);
}
type = task.keyPath;
ref = task.implicitSlot;
null !== key
? (task.keyPath = null === type ? key : type + "," + key)
: null === type && (task.implicitSlot = !0);
request = renderModelDestructive(request, task, emptyRoot, "", props);
task.keyPath = type;
task.implicitSlot = ref;
return request;
}
if ("function" === typeof type)
return isClientReference(type)
? renderClientElement(task, type, key, props)
: renderFunctionComponent(request, task, key, type, props);
if ("string" === typeof type)
return renderClientElement(task, type, key, props);
if ("symbol" === typeof type)
Expand All @@ -615,23 +617,7 @@ function renderElement(request, task, type, key, ref, props) {
type = init(type._payload);
return renderElement(request, task, type, key, ref, props);
case REACT_FORWARD_REF_TYPE:
return (
(type = type.render),
(ref = task.thenableState),
(task.thenableState = null),
(thenableIndexCounter = 0),
(thenableState = ref),
(ref = type(props, void 0)),
(props = task.keyPath),
(type = task.implicitSlot),
null !== key
? (task.keyPath = null === props ? key : props + "," + key)
: null === props && (task.implicitSlot = !0),
(request = renderModelDestructive(request, task, emptyRoot, "", ref)),
(task.keyPath = props),
(task.implicitSlot = type),
request
);
return renderFunctionComponent(request, task, key, type.render, props);
case REACT_MEMO_TYPE:
return renderElement(request, task, type.type, key, ref, props);
}
Expand Down Expand Up @@ -928,18 +914,18 @@ function renderModelDestructive(
}
if ("symbol" === typeof value) {
task = request.writtenSymbols;
var existingId$15 = task.get(value);
if (void 0 !== existingId$15) return serializeByValueID(existingId$15);
existingId$15 = value.description;
if (Symbol.for(existingId$15) !== value)
var existingId$8 = task.get(value);
if (void 0 !== existingId$8) return serializeByValueID(existingId$8);
existingId$8 = value.description;
if (Symbol.for(existingId$8) !== value)
throw Error(
"Only global symbols received from Symbol.for(...) can be passed to Client Components. The symbol Symbol.for(" +
(value.description + ") cannot be found among global symbols.") +
describeObjectForErrorMessage(parent, parentPropertyName)
);
request.pendingChunks++;
parent = request.nextChunkId++;
parentPropertyName = stringify("$S" + existingId$15);
parentPropertyName = stringify("$S" + existingId$8);
parentPropertyName = parent.toString(16) + ":" + parentPropertyName + "\n";
request.completedImportChunks.push(parentPropertyName);
task.set(value, parent);
Expand Down
2 changes: 1 addition & 1 deletion compiled/facebook-www/ReactTestRenderer-dev.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -26083,7 +26083,7 @@ if (__DEV__) {
return root;
}

var ReactVersion = "18.3.0-www-modern-6f034660";
var ReactVersion = "18.3.0-www-modern-c1410230";

// Might add PROFILE later.

Expand Down

0 comments on commit 7a01513

Please sign in to comment.