React Slots: Dynamic Sidebars and Headers Without the Mess
The Problem
You have a layout with a sidebar. Different pages need different sidebar content. The obvious solutions all have tradeoffs:
Prop drilling: Pass sidebar content up through every layer. Verbose, brittle.
Global state: Store sidebar config in Redux/Zustand. Now your UI is in a store.
Render props: <Layout sidebar={<Filters />}>. Works until you need sidebar content that depends on state defined inside the page.
That last point is the killer. Your filters component needs access to state that lives deep in your page component. Now what?
The Solution
A slot is a named portal target discovered at runtime via React context.
import { createContext, useContext, useState, PropsWithChildren } from 'react'
import { createPortal } from 'react-dom'
type SlotState = [
HTMLDivElement | null,
React.Dispatch<React.SetStateAction<HTMLDivElement | null>>,
]
export function createSlot(name: string) {
const Context = createContext<SlotState | null>(null)
function useSlot() {
const ctx = useContext(Context)
if (!ctx) {
throw new Error(`${name} must be used within ${name}.Provider`)
}
return ctx
}
function Provider({ children }: PropsWithChildren) {
const slotState = useState<HTMLDivElement | null>(null)
return <Context.Provider value={slotState}>{children}</Context.Provider>
}
function Slot(props: React.HTMLAttributes<HTMLDivElement>) {
const [, setSlot] = useSlot()
return <div {...props} ref={setSlot} />
}
function Portal({ children }: PropsWithChildren) {
const [slot] = useSlot()
return slot ? createPortal(children, slot) : null
}
return { Provider, Slot, Portal }
}
Usage
const Sidebar = createSlot('Sidebar')
function Layout({ children }: PropsWithChildren) {
return (
<Sidebar.Provider>
<div className="flex">
<aside>
<Sidebar.Slot />
</aside>
<main>{children}</main>
</div>
</Sidebar.Provider>
)
}
function SearchPage() {
const [filters, setFilters] = useState({ date: 'any', type: 'all' })
const results = useSearch(filters)
return (
<>
<ResultsList results={results} />
<Sidebar.Portal>
<Filters value={filters} onChange={setFilters} />
<ResultCount count={results.length} />
</Sidebar.Portal>
</>
)
}
The filters live in the sidebar visually, but in SearchPage logically. State stays colocated with the component that uses it.
Why This Works
React has two hierarchies: the component tree (data flow) and the DOM tree (visual layout). They're usually the same. Portals let you separate them.
Standard portals target fixed DOM nodes like document.body. This pattern targets a dynamic node discovered via context. The slot registers itself with a ref. The portal finds it through the same context.
Result: components inject content into arbitrary layout positions while preserving normal React data flow. Context, state, handlers — everything works as expected.
When to Use This
Sidebars with page-specific content
Header actions that depend on page state
Toolbars that change based on selection
Any layout where content source and content location should be decoupled
When Not To
If your sidebar content doesn't need state from inside the page, simpler patterns work fine. Render props or composition handle static content without the indirection.

