Adding Server Component support to DevTools is going to be a big lift, but generally speaking there are a few initial things to work out:
- Updating the Flight server renderer to stream server component names/types to the client.
- Adding a new hook to DevTools for the Flight client to call.
- Merging the server component subtrees into the DevTools client tree.
Yesterday and today I've been thinking about this last item, and I'm feeling pretty stumped at the moment. Merging the trees isn't that difficult, but preserving the tree across client updates gets nasty when it comes to things like conditionally rendered elements.
Setting performance concerns aside, even if I were to undo the merged trees, apply the client updates, and then redo the mergeā I'm still not sure we would definitely end up with the correct final state.
For example, consider the following client component that accepts "children" rendered by a server component:
function StatefulClientComponent({ children = null }) {
// State...
return (
<React.Fragment>
{condition && <ClientComponent key="A" />}
{children}
{condition && <ClientComponent key="B" />}
</React.Fragment>
);
}
First, DevTools would see this tree as only:
<StatefulClientComponent>
Then server component info could be added in, e.g.
<StatefulClientComponent>
<ServerComponent>
If condition
were to be true, DevTools should update its (merged) tree to:
<StatefulClientComponent>
<ClientComponent key="A">
<ServerComponent>
<ClientComponent key="B">
But I'm not sure how it should know that "A" goes before ServerComponent
and "C" goes after.
The way DevTools appends children further complicates things: First it sends "create" commands for new children, then it sends a "reorder" command (consisting of only the child ids). In this case, that means that the reorder command would only specify "A" and "B" (and DevTools would need to know to leave the ServerComponent
in place).
Maybe that's not so difficult to do in this simple example, but what if the server component rendered content (passed as children
) contained nested client components? Then the initial DevTools tree might look something like this:
<StatefulClientComponent>
<NestedClientComponent>
And the merged tree would look like this:
<StatefulClientComponent>
<ServerComponent>
<NestedClientComponent>
Now when there's an update, DevTools would tell the frontend that StatefulClientComponent
has 3 children: "A", NestedClientComponent
, and "B". So DevTools would need to both ignore children that had previously been reparented and ignore server components when re-ordering.
Rather than merging the two trees, maybe the thing to do here is to store a separate, parallel data structure. For example, given the following app code:
DevTools would generate the following (client) tree initially:
We could merge the two easily, but then if conditional children were rendered such that the app became:
Then it would be difficult to apply this update to the already-merged tree. But, if instead of merging the trees, we stored separate metadata that e.g. the edge between client components
ClientA
andClientB
should bereplacedoverridden with an edge betweenClientA
andServerA
, and we updated the index-to-tree generation logic to account for this metadata as well, then the update could be applied to the original client tree without interfering with the merge.There may be some trickiness in terms of updating the weight of nodes in the tree, mapping element-to-index, removing children, etc. but I think this direction may be more promising.