README
@validdata.de/react-content-slots
Place slots in your layout and define their content (rendered elements, data...) somewhere else in the component tree.
Use cases
Often in React applications you'll want to define the content of a certain section on the page at a completely different place in the component tree. For example, consider an application that shows either a list of products or the details of a single product.
That is, when showing the list of products, the component tree may look like this:
Layout
/ \
Products Toolbar
|
List
And when a product is selected from the list, the Products
component render a Details
component as its child instead:
Layout
/ \
Products Toolbar
|
Details
Here's the pseudo-code of such an app:
const Layout = () => (
<>
<Toolbar/>
<Product/>
</>
)
const Toolbar = () => (
<div>...</div>
)
const Products = () => {
const [view, setView] = useState('list');
return <>
<button onClick={() => setView('list')}>To List</button>
<button onClick={() => setView('details')}>To Details</button>
{ view === 'list' ? <List/> : <Details/> }
</>
}
const List = () => ( <table>...</table> )
const Details = () => ( <section><h2>Details</h2>...</section> )
Now imagine you'd want the content of Toolbar
to change between the two cases. The information which view is currently displayed (List
or Details
) is stored in Products
. Layout
- the component responsible for rendering Toolbar
does not know about this and thus cannot easily fill in the content of Toolbar
.
That's where react-content-slots
comes into play. With it you can define 'slots' that hold certain content (e.g. React elements or other data). Wherever needed the content of a slot can be accessed and used appropriately (e.g. by rendering it). At other places you can inject the content into the slot. You can define the slot content at multiple places in the component tree and the definition the lowest in the tree will be the one that is actually used.
What would this look like in our example? You need four things
- A definition of the slot, similar to the definition of a context in React:
createSlot
- One of two ways define the content of the slot: either the
useSlot
hook or theSlotContent
component. - One of two ways to access the content of the slot: either the
useSlotContent
hook or theSlotOutlet
component - A
SlotProvider
which is placed at a common ancestor of any component that usesuseSlot
/SlotContent
/useSlotContent
/SlotOutlet
// 1. The slot definition
const ToolbarSlot = createSlot();
// 2. A place where the slot content is used
const Toolbar = () => (
<div><SlotOutlet slot={ToolbarSlot} /></div>
)
const Products = () => {
const [view, setView] = useState('list');
return <React.Fragment>
<button onClick={() => setView('list')}>To List</button>
<button onClick={() => setView('details')}>To Details</button>
{ view === 'list' ? <List/> : <Details/> }
</React.Fragment>
}
// 3. A definition of the slot content
const List = () => (
<React.Fragment>
<SlotContent slot={ToolbarSlot}>
<button>Add</button>
<input placeholder="Search..." />
</SlotContent>
<table>...</table>
</React.Fragment>
)
// 3 (contd., optional). Maybe another definition of the slot content -->
const Details = () => (
<section>
<SlotContent slot={ToolbarSlot}>
<button>Delete</button>
<button>Print</button>
</SlotContent>
<h2>Details</h2>
...
</section>
)
// 4. A SlotProvider tying everything together
const Layout = () => (
<SlotProvider slot={ToolbarSlot}>
<Toolbar/>
<Products/>
</SlotProvider>
)
Caveats
Never use the
useSlotContent
hook at an ancestor of any component that usesSlotContent
oruseSlot
for the same slot. Doing so will cause an infinite rendering loop inside React. Workarounds:- If possible, use
SlotOutlet
. While inside it usesuseSlotContent
, being in a distinct component breaks the ancestral line toSlotContent
/useSlot
- Otherwise, extract the part that uses
useSlotContent
into a component and render that component instead. This will break the ancestral line betweenuseSlotContent
andSlotContent
/useSlot
The reason for this behavior is easy to explain: when
useSlot
is called (whichSlotContent
uses internally) it may potentially update the current value of slot.This will all components that useuseSlotContent
to re-render. If the component that calleduseSlot
is a descendant of theuseSlotContent
use it may be re-rendered itself, causing a call touseSlot
that may update the slot value, which leads to a re-render of theuseSlotContent
component etc.- If possible, use
Never render
SlotContent
conditionally. That is, if you render it once, make sure to always render it. Treat it like a hook in this regard. Conditionally renderingSlotContent
will mess up the content stack. If you need to conditionally (not) overwrite parent content, use thedisable
prop ofSlotContent
.
API
createSlot(defaultValue?)
Returns a new slot. Optionally, the function accepts a default value. If no explicit default value is provided, null
is used.
NB for TypeScript users: you can (and should) provide a type to createSlot
to determine the type of the content. If no type is provided, ReactNode
is assumed. If a default value is provided the type can be inferred. However, if the provided default value is a React element, you should still set ReactNode
as the type argument, otherwise it's easy to run into type issues when using SlotContent
.
Parameters
defaultValue
(optional): the default value for the slot when no other content has been defined.
Returns
- A slot variable that is used by most of the other functions and components in this library.
Examples:
const title = createSlot('Default title');
const content = createSlot();
const notRecommended = createSlot(<p>Default</p>);
const recommended = createSlot<ReactNode>(<p>Default</p>);
<SlotProvider>
A context provided that has to be placed at a common ancestor of all components that use useSlot
or useSlotContent
directly or indirectly. For each slot, you should have at least one SlotProvider
in your app (or use SlotProviders
for brevity). SlotProvider
can be nested like all context providers to redefine the slot system at a specific subtree of the component tree.
Props
slot
: a slot variable created withcreateSlot
children
: the subtree that uses the provided slot
Example
import {createSlot, SlotProvider} from '@validdata.de/react-content-slots';
export const TitleSlot = createSlot('Default title');
const App = () => {
return (
<SlotProvider slot={TitleSlot}>
... Rest of the app
</SlotProvider>
)
}
<SlotProviders>
A convenience component that allows multiple slots to be provided. This exists only to avoid nesting of a bunch of SlotProvider
s when multiple slots are used in an application.
Props
slots
: an array of slot variables created withcreateSlot
. For performance reasons this should be constant or at least memoized somehow.children
: the subtree that uses the provided slot
import {createSlot, SlotProviders} from '@validdata.de/react-content-slots';
export const TitleSlot = createSlot('Default title');
export const ContentSlot = createSlot();
const allSlots = [TitleSlot, ContentSlot];
const App = () => {
return (
<SlotProviders slots={allSlots}>
... Rest of the app
</SlotProviders>
)
}
useSlot(slot, content, disable?)
Set the content of a slot.
Parameters
slot
: the slot for which the content should be definedcontent
: the new contentdisable
(optional): iftrue
, the content will not actually be used. Instead, the parent content or the default content will be used. Useful if you only want to conditionally overwrite parent content.
Example
const MyPage = () => {
useContent(AppBarSlot, <p>This content is captured here and used somewhere </p>);
useContent(TitleSlot, "My awesome page");
return (...)
}
<SlotContent>
Set the content of a slot. If you prefer the "thinking in components" approach, you can use this component instead of useSlot
. Uses useSlot
internally.
Props
slot
: the slot for which the content should be definedcontent
orchildren
: the new content. If the content prop is present, it will be used. Otherwise, the children are used as the content.disable
(optional): iftrue
, the content will not actually be used. Instead, the parent content or the default content will be used. Useful if you only want to conditionally overwrite parent content.
Example
const MyPage = () => {
return (
<div>
<p>Normal page content</p>
<SlotContent slot={AppBarSlot}>
<p>This content is captured here and used somewhere else.</p>
</SlotContent>
<SlotContent slot={TitleSlot} content="My awesome page"/>
<SlotContent slot={OtheSlot} disable={someCondition}>
<p>This content will not overwrite its parent if "someCondition"
is true. </p>
</SlotContent>
</div>
)
}
useSlotContent(slot)
Return the current content of a slot.
Parameters
slot
: the slot for which the current content should be returned
Returns
An object with following fields:
{
content,
id,
version
}
content
: the content set by the deepest component in the treeid
: an id to the current place of the slot on the content stack. The id does not change when the content change, but it changes if a different source (a different component usingSlotContent
/useSlot
) is used for the content. Note that when the same component unmounts and later mounts again, the id associated with the content provided by this componen will be differnt.version
: the number of times the content for the current id has changed
<SlotOutlet>
Renders the current content of a slot as its children. Uses useSlotContent
internally.
Props
slot
: the slot for which the current content should be renderedchildren
(optional): if ommitted, the slot content itself is rendered as the children ofSlotOutlet
. Note that in that case the content should be compatible withReactNode
, otherwise you will get runtime errors. If provided,children
needs to be a function that accepts as an argument the object returned byuseSlotContent
(see above).
Examples
const MyPage = () => {
return <>
<SlotContent slot={AppBarSlot}/>
<SlotContent slot={TitleSlot}>
{({id, version, content}) => (
<p>
The current title is {content}.
[
id: {id},
version: {version}
]
</p>
)}
</SlotContent>
</>
}