Description
There’s a powerful pattern in React called Slot and Fill which allows the insertion of any React component in different places of the app. It is perfect when the application is extensible, like Frontity.
In the case of Frontity, this will allow the theme developer to insert <Slot>
components among the theme, and other package developers to add <Fill>
components hooked to a particular Slot.
User Stories
As a Frontity theme consumer
I want to add additional components to the theme
so that I can customize to my needs
As a Frontity theme consumer
I want to substitute certain components of the theme
so that I can completely override parts of the theme
As a Frontity theme consumer
I want to specify some data assertions for my fills
so that I can fill the slots only under certain conditions
Moved to: Data assertions for Fills
As a Frontity theme creator
I want to use a hook instead of a component for Slots
so that I can have more control of the insides
Moved to: useFills hooks
Examples
- Add an ad above the first post of any archive.
- Add a newsletter subscription widget after the eighth post of the home.
- Add more elements to a menu, for example, a link to the GDPR or a notifications switch.
- Substitute the theme footer for a new custom one.
Possible solution
Initial proposed solution (click to open)
Slots in the theme
The theme developers can add Slots wherever they want.
import { Slot } from "frontity";
const Menu = () => {
return (
<>
<Slot name="before-menu" />
<MenuContainer>
<MenuItem1 />
<MenuItem2 />
<MenuItem3 />
<Slot name="menu-items" />
</MenuContainer>
<Slot name="after-menu" />
</>
)
};
Themes need to export a list of Slots to be used in the upcoming Frontity Admin UI.
Slots in the content
Slots in the content don’t need to be added by the theme because any package can add processors.
Possible Solutions:
- Add common Slot processors to
html2react
for things like:"after-third-paragraph"
"before-first-image"
"after 500 characters"
This would need the ability to add components instead of substituting them in html2react
. I don’t know if this is possible.
import { afterThirdParagraph } from "@frontity/html2react/processors/slots";
{
...
libraries: {
html2react: {
processors: [image, afterThirdParagraph]
}
}
}
- Add multiple Slot processors at once:
import slots from "@frontity/html2react/processors/slots";
{
...
libraries: {
html2react: {
processors: [image, slots]
}
}
}
- Let “Fill package” deal with this themselves:
packages: [
...
{
name: "some-ads-package",
state: {
fills: [
{
name: "ad-1",
id: "123456",
slot: "content-after-third-paragraph"
}
]
}
}
]
Passing props to Fills
Decisions:
- How to pass props to
Fills
? - Are there some standard props that need to be passed or is it up to the theme and
Slot
to decide?
How to pass props to Fills
?
-
By including the data in the
Slot
.- Pro: It is simpler.
const SomeSlot = ({ state }) => {
return (
<Slot name="footer" data={state.source.get("/some-page")} />
)
};
-
By using a render prop.
- Pro: It is more hackable.
const SomeSlot = ({ state }) => {
return (
<Slot name="footer">
{fills =>
fills.map(Fill => <Fill data={state.source.get("/some-page")} />
}
</Slot>
)
};
In both cases, the props are passed down to any direct children of Fill
, like Page
here:
const SomeFill = ({ state }) => {
return (
<Fill name="footer">
<Page>
</Fill>
)
};
Post
now has access to data
.
const Page = ({ state, data }) => {
const page = state.source.page[data.id];
return <div>{page.title.rendered}</div>;
};
Although this is a bit too magical for the developer creating the Fill
. Maybe a render prop could be used instead:
const SomeFill = ({ state }) => {
return (
<Fill name="footer">
{({ data }) => <Page data={data}>
</Fill>
)
};
We need to take a good look at how TypeScript can work here.
Slot substitution
- Fallback to a component by passing a default component as
children
.- Pro: It is simpler.
const SomeSlot = () => {
return (
<Slot name="footer">
<DefaultFooter />
</Slot>
)
};
- Fallback to a component by checking the length of
fills
.- Pro: It is more hackable.
const SomeSlot = () => {
return (
<Slot name="footer">
{({ fills }) => {
if (fills.length === 0) return <DefaultFooter />;
return fills.map(Fill => <Fill />);
}}
</Slot>
)
};
- This could be the default
children
if no render prop is passed:
const SomeSlot = () => {
return (
<Slot name="footer">
{fills => fills.map(Fill => <Fill />)}
</Slot>
)
};
Which is actually equivalent to this, but somehow seems simpler to understand what’s going on:
const SomeSlot = () => {
return (
<Slot name="footer">
{fills => fills}
</Slot>
)
};
References
- This is the library that started the pattern: https://github.com/camwest/react-slot-fill
As far as I know, it is still based on the old context API. - Gutenberg has its own implementation, based on camwest library: https://github.com/WordPress/gutenberg/tree/master/packages/components/src/slot-fill
It’s a modern implementation of the same, so it’s probably great to take a look. - Javi Velasco made a very similar one: https://github.com/javivelasco/react-tunnels
- There’s another library made by a guy named Jonatan Salas: https://github.com/BlackBoxVision/react-slot-fill
- Jason Miller did a SlotFill library for Preact as well: https://github.com/developit/preact-slots