Skip to content

Commit

Permalink
[Flight] Support Keyed Server Components (#28123)
Browse files Browse the repository at this point in the history
Conceptually a Server Component in the tree is the same as a Client
Component.

When we render a Server Component with a key, that key should be used as
part of the reconciliation process to ensure the children's state are
preserved when they move in a set. The key of a child should also be
used to clear the state of the children when that key changes.

Conversely, if a Server Component doesn't have a key it should get an
implicit key based on the slot number. It should not inherit the key of
its children since the children don't know if that would collide with
other keys in the set the Server Component is rendered in.

A Client Component also has an identity based on the function's
implementation type. That mainly has to do with the state (or future
state after a refactor) that Component might contain. To transfer state
between two implementations it needs to be of the same state type. This
is not a concern for a Server Components since they never have state so
identity doesn't matter.

A Component returns a set of children. If it returns a single child,
that's the same as returning a fragment of one child. So if you
conditionally return a single child or a fragment, they should
technically reconcile against each other.

The simple way to do this is to simply emit a Fragment for every Server
Component. That would be correct in all cases. Unfortunately that is
also unfortunate since it bloats the payload in the common cases. It
also means that Fiber creates an extra indirection in the runtime.

Ideally we want to fold Server Component aways into zero cost on the
client. At least where possible. The common cases are that you don't
specify a key on a single return child, and that you do specify a key on
a Server Component in a dynamic set.

The approach in this PR treats a Server Component that returns other
Server Components or Lazy Nodes as a sequence that can be folded away.
I.e. the parts that don't generate any output in the RSC payload.
Instead, it keeps track of their keys on an internal "context". Which
gets reset after each new reified JSON node gets rendered.

Then we transfer the accumulated keys from any parent Server Components
onto the child element. In the simple case, the child just inherits the
key of the parent.

If the Server Component itself is keyless but a child isn't, we have to
add a wrapper fragment to ensure that this fragment gets the implicit
key but we can still use the key to reset state. This is unusual though
because typically if you keyed something it's because it was already in
a fragment.

In the case a Server Component is keyed but forks its children using a
fragment, we need to key that fragment so that the whole set can move
around as one. In theory this could be flattened into a parent array but
that gets tricky if something suspends, because then we can't send the
siblings early.

The main downside of this approach is that switching between single
child and fragment in a Server Component isn't always going to reconcile
against each other. That's because if we saw a single child first, we'd
have to add the fragment preemptively in case it forks later. This
semantic of React isn't very well known anyway and it might be ok to
break it here for pragmatic reasons. The tests document this
discrepancy.

Another compromise of this approach is that when combining keys we don't
escape them fully. We instead just use a simple `,` separated concat.
This is probably good enough in practice. Additionally, since we don't
encode the implicit 0 index slot key, you can move things around between
parents which shouldn't really reconcile but does. This keeps the keys
shorter and more human readable.

DiffTrain build for commit 95ec128.
  • Loading branch information
sebmarkbage committed Feb 5, 2024
1 parent 4dc539f commit fc7d1d5
Show file tree
Hide file tree
Showing 7 changed files with 9 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25622,7 +25622,7 @@ if (__DEV__) {
return root;
}

var ReactVersion = "18.3.0-canary-86b95edb0-20240205";
var ReactVersion = "18.3.0-canary-95ec12839-20240205";

// Might add PROFILE later.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9171,7 +9171,7 @@ var devToolsConfig$jscomp$inline_1012 = {
throw Error("TestRenderer does not support findFiberByHostInstance()");
},
bundleType: 0,
version: "18.3.0-canary-86b95edb0-20240205",
version: "18.3.0-canary-95ec12839-20240205",
rendererPackageName: "react-test-renderer"
};
var internals$jscomp$inline_1190 = {
Expand Down Expand Up @@ -9202,7 +9202,7 @@ var internals$jscomp$inline_1190 = {
scheduleRoot: null,
setRefreshHandler: null,
getCurrentFiber: null,
reconcilerVersion: "18.3.0-canary-86b95edb0-20240205"
reconcilerVersion: "18.3.0-canary-95ec12839-20240205"
};
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
var hook$jscomp$inline_1191 = __REACT_DEVTOOLS_GLOBAL_HOOK__;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9599,7 +9599,7 @@ var devToolsConfig$jscomp$inline_1054 = {
throw Error("TestRenderer does not support findFiberByHostInstance()");
},
bundleType: 0,
version: "18.3.0-canary-86b95edb0-20240205",
version: "18.3.0-canary-95ec12839-20240205",
rendererPackageName: "react-test-renderer"
};
var internals$jscomp$inline_1231 = {
Expand Down Expand Up @@ -9630,7 +9630,7 @@ var internals$jscomp$inline_1231 = {
scheduleRoot: null,
setRefreshHandler: null,
getCurrentFiber: null,
reconcilerVersion: "18.3.0-canary-86b95edb0-20240205"
reconcilerVersion: "18.3.0-canary-95ec12839-20240205"
};
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
var hook$jscomp$inline_1232 = __REACT_DEVTOOLS_GLOBAL_HOOK__;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ if (__DEV__) {
) {
__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error());
}
var ReactVersion = "18.3.0-canary-86b95edb0-20240205";
var ReactVersion = "18.3.0-canary-95ec12839-20240205";

// ATTENTION
// When adding new symbols to this file,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -545,4 +545,4 @@ exports.useSyncExternalStore = function (
exports.useTransition = function () {
return ReactCurrentDispatcher.current.useTransition();
};
exports.version = "18.3.0-canary-86b95edb0-20240205";
exports.version = "18.3.0-canary-95ec12839-20240205";
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,7 @@ exports.useSyncExternalStore = function (
exports.useTransition = function () {
return ReactCurrentDispatcher.current.useTransition();
};
exports.version = "18.3.0-canary-86b95edb0-20240205";
exports.version = "18.3.0-canary-95ec12839-20240205";
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
"function" ===
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
86b95edb0ae68a68f7611aca56d3139e7934fb75
95ec128399a8b34884cc6bd90a041e03ce5c1844

0 comments on commit fc7d1d5

Please sign in to comment.