-
Notifications
You must be signed in to change notification settings - Fork 11
Children slots in client components #62
Comments
For inner blocks, as alternative to the HTML comments as separators: 5. Text nodes with a set of non-printable unicode characters
<h3>Some description</h3>
<!-- slot --> ⌷⌷⌷⌷
<div>I'm a child content</div>
<img src="with-an-image.png" />
<!-- /slot --> ⌷⌷⌷⌷ 6.
|
Nice ideas. Thanks, Peter 🙂 I especially like the comments + invisible characters idea. I want to introduce another constraint: nested slots Blocks can't nest other blocks in their implementations, but client components can. Client components are related to the task Experiment with ways not to hydrate the entire block but only some client components from the Tracking issue. Imagine these two client components, one nesting the other: const ClientComp1 = ({ children }) => {
// some logic...
return (
<client-comp-1>
<div class="from-client-comp-1">{children}</div>
</client-comp-1>
);
}; const ClientComp2 = ({ children }) => {
// some logic...
return (
<client-comp-2>
<client-comp-1>
<div class="from-client-comp-2">{children}</div>
</client-comp-1>
</client-comp-2>
);
}; If you use const MyBlock = () => (
<client-comp-2>
<div>my block content</div>
</client-comp-2>
); You would end up with this SSRed HTML, where the slots are reversed: <client-comp-2>
<client-comp-1>
<div class="from-client-comp-1">
<!-- slot -->
<div class="from-client-comp-2">
<!-- slot -->
<div>my block content</div>
<!-- /slot -->
</div>
<!-- /slot -->
</div>
</client-comp-1>
</client-comp-2> And the current algorithm wouldn't know how to differentiate between the slots. I see two possible solutions so far:
Coming back to Peter's comments + invisible characters idea: Maybe we could rely on comments for the slot position and, at the last moment, add the invisible characters dynamically with PHP:
Also, if we need to add information to the slots, like a unique ID, we need to know that we can encode that information within the invisible characters and then decode it back. I'll ask @dmsnell about it. He's been studying them for the dynamic token implementation (although he finally discarded them). |
Another constraint I'd like us to put in the basket is compatibility with multiple slots. Again, this is something more for client components than for blocks, although it could end up being used in blocks too. Imagine this client component: const ClientComp = ({ children, header, footer }) => {
// ...
return (
<client-comp>
<header>{header}</header>
<main>{children}</main>
<footer>{footer}</footer>
</client-comp>
);
}; Something like this would need to be the SSRed output: <client-comp>
<header>
<slot name="header">
<h1>This is the header</h1>
</slot>
</header>
<main>
<slot name="children">
<div>This is the main</div>
</slot>
</main>
<footer>
<slot name="footer">
<div>This is the footer</div>
</slot>
</footer>
</client-comp> If we'd ever want to support multiple slots and the ability to use these atomic client components inside one another, we could not use the algorithm I described in my previous comment (point 2.) because there would be an undetermined number of slots. We would need the unique id/hash solution (point 1.). I've also been thinking about how we could abstract the insertion of these slots. Maybe we could use a We also need to avoid the typical It's indeed the "initially hidden children" problem of inner blocks (related to this issue), but generalized for smaller islands/client-component. const propsContext = createContext(null);
const Wrapper = (props) => {
<propsContext.Provider value={props}>
<Comp {...props} />
</propsContext.Provider>;
};
const Slot = ({ name, show }) => {
const props = useContext(propsContext);
return show ? (
<slot name={name} show={show} hash={`slot-${props.hash}`}>
{props[name]}
</slot>
) : (
<template slot={name} show={show} hash={`slot-${props.hash}`}>
{props[name]}
</template>
);
};
// And the client component would do this:
const MyClientComponent = () => {
const [show, setShow] = useState(false);
return (
<div>
Some content
<Slot name="children" show={show} />
</div>
);
}; |
I think we need a different solution for blocks (islands) and client components. Blocks can't be nested, and they usually have a single wrapper node (although it doesn't seem to be mandatory). So I think we can use the attribute approach. I wrote about it on |
EDIT: After realizing that internal markers are redundant when combined with slots with unique ids/hashes, and that some sort of This is my proposal method to hydrate atomic/independent client components that support:
View internal node markers proposal1. Mark internal nodeFirst, we start annotating all the internal nodes of a client component, as opposed to the wrapper nodes of the children. Something like this: const ClientComp = ({ children }) => {
// some logic...
return (
<client-comp>
<div>
<div>{children}</div>
</div>
</client-comp>
);
}; const MyBlock = () => (
<client-comp>
<div>my block content</div>
</client-comp>
); The SSRed HTML would be: <client-comp wp-comp>
<div wp-internal>
<div wp-internal>
<div>my block content</div>
</div>
</div>
</client-comp> The algorithm can discard all the internal nodes and pass the rest as children. 2. Use unique idsMarking the internal nodes doesn't solve nested client components. We also need to add a unique hash/id. Imagine these client components: const ClientComp1 = ({ children }) => {
// some logic...
return (
<client-comp-1>
<div>{children}</div>
</client-comp-1>
);
}; const ClientComp2 = ({ children }) => {
// some logic...
return (
<client-comp-2>
<client-comp-1>
<div>{children}</div>
</client-comp-1>
</client-comp-2>
);
}; If you use const MyBlock = () => (
<client-comp-2>
<div>my block content</div>
</client-comp-2>
); You would end up with this SSRed HTML: <client-comp-2 wp-comp>
<client-comp-1 wp-comp wp-internal>
<div wp-internal>
<div wp-internal>
<div>my block content</div>
</div>
</div>
</client-comp-1>
</client-comp-2> We neet to add a hash to the component ( <client-comp-2 wc-123>
<client-comp-1 wc-456 wi-123>
<div wi-456>
<div wi-123>
<div>my block content</div>
</div>
</div>
</client-comp-1>
</client-comp-2> Now the algorithm can do the following:
And for
It also works for slots that are not in the same branch. Multiple branches exampleconst ClientComp1 = ({ children }) => {
// some logic...
return (
<client-comp-1>
<div>{children}</div>
</client-comp-1>
);
}; const ClientComp2 = ({ children }) => {
// some logic...
return (
<client-comp-2>
<client-comp-1>
<div>child of client-comp-1</div>
</client-comp-1>
<div>{children}</div>
</client-comp-2>
);
}; <client-comp-2 wc-123>
<client-comp-1 wc-456 wi-123>
<div wi-456>
<div wi-123>child of client-comp-1</div>
</div>
</client-comp-1>
<div wi-123>
<div>my block content</div>
</div>
</client-comp-2> For
3. Identify
|
I've been talking with @DAreRodz, and it turns out that if adding the |
Closing this for now, as this is only related to components that are hydrated in the client and output more than one tag. We'll open again in the future if/when we explore this. |
Moved from #60.
EDIT: Although this issue started as a way to figure out how to identify
children
in islands and client components, let's use this use to figure out how to fully hydrate client components, including nested components, multiple slots and out-of-order hydration.Let's analyze what options we have to identify
children
:<!-- slot --><div>...</div><!-- slot -->
<slot>
<div wp-children>
1. Use HTML comments
Many WordPress cache and optimization plugins and CDNs (including the popular Cloudflare) remove the HTML comments, so I'd try to avoid this option.
As reference, Fresh is using comments to find its islands, although it's a bit weird because they don't hydrate them and they don't support children.
2. Use a wrapper
A wrapper is reliable, but as we've seen with
wp-block
,display: contents
is not the holy grail and they need to be taken into account because they affect CSS.For interactive blocks, we could use the same
<wp-inner-blocks>
approach.For client components (smaller than blocks), we could abstract it in the creation of the component, but the SSR should add it. Imagine this client component:
The SSR should be:
So maybe we should not abstract it in the JSX, and make it explicit:
We could add
slot[name=children] { display: contents }
by default, but again, other CSS like*:nth-child()
requires taking them into account.3. Use an attribute
We would need to add a special attribute to all the parent nodes:
This would not need to be as explicit because it doesn't affect CSS so I think it could be nicely abstracted.
The have two problems:
Attributes need an HTML node
So we would have to detect texts and wrap them with
<span>
s:We can't add them on the fly using PHP
Imagine you have these
$children
, and you want to add the attributes:We would need proper HTML parsing to know that the top-level tags are only those with the classes
title
,description
andextra
.By the conversation on the WP_HTML_Walker pull request, I think we can assume that the API of the
WP_HTML_Walker
is as closer as we will ever get to HTML parsing. And that API is not capable of adding these attributes (it just finds tags; it doesn't know which tags are in the top-level).On the other hand, wrapping
$children
with<slot>
it's a simple as:4. Figure out by comparing the DOM with the component output
That'd be similar to what the
hydrate
algorithm does: compare the DOM with the vDOM generated by the app and try to match it. In this case, instead of removing what doesn't match, it could treat it as children.This solution is more complex, requires more computation, and a bigger initial bundle, so I'd try to avoid it if we can.
Any other ideas? 🙂
The text was updated successfully, but these errors were encountered: