Tiny React Portal slots (Provider + named Slot & Portal factory).
React components often need to render content in different parts of the UI tree. Traditional approaches lead to prop drilling, tight coupling, and layout constraints.
react-portalslots provides:
- Decoupled rendering: Render content anywhere, display it elsewhere
- Named slots: Semantic slots (header, footer, sidebar) that components can target
- Type safety: Full TypeScript support
- Minimal API: Just a provider and factory function
Perfect for layout systems, component libraries, and avoiding prop drilling.
npm install react-portalslots
# or
pnpm add react-portalslots
# or
yarn add react-portalslots
# or
bun add react-portalslotsimport type { PropsWithChildren } from 'react';
import { PortalSlotsProvider, PortalSlot } from 'react-portalslots';
const HeaderPortal = PortalSlot('header');
const FooterPortal = PortalSlot('footer');
function Layout({ children }: PropsWithChildren) {
return (
<div className="page">
<header className="page-header">
<HeaderPortal.Slot />
</header>
<main className="page-content">{children}</main>
<footer className="page-footer">
<FooterPortal.Slot />
</footer>
</div>
);
}
export function App() {
return (
<PortalSlotsProvider>
<Layout>
{/* These can live anywhere in the tree */}
<HeaderPortal>
<button>Save</button>
</HeaderPortal>
<FooterPortal>
<small>© 2025</small>
</FooterPortal>
{/* App content */}
<div>Dashboard</div>
</Layout>
</PortalSlotsProvider>
);
}import type { PropsWithChildren, ReactNode } from 'react';
type LayoutProps = {
header?: ReactNode;
footer?: ReactNode;
};
function Layout({ header, footer, children }: PropsWithChildren<LayoutProps>) {
return (
<div className="page">
<header className="page-header">{header}</header>
<main className="page-content">{children}</main>
<footer className="page-footer">{footer}</footer>
</div>
);
}
export function App() {
// Content that wants to render into the header/footer must be lifted up here
// from deep components, causing prop drilling and tight coupling.
return (
<Layout
header={<button>Save</button>}
footer={<small>© 2025</small>}
>
<SomeToolbar />
</Layout>
);
}
function SomeToolbar() {
// Cannot push content into the header without threading callbacks/state
// through multiple layers or using a global store (which is brittle).
return null;
}- Drawbacks: prop drilling, implicit coupling, awkward lifting of state/UI, hard reuse/testing.
Context provider that must wrap your application.
<PortalSlotsProvider>
<App />
</PortalSlotsProvider>Factory function that creates a pair of components for a named slot.
- PortalSlot.Slot: The slot container where content will be rendered
- PortalSlot: Portal component that renders content into the slot
const HeaderPortal = PortalSlot('header');
// Use the slot in your layout
<HeaderPortal.Slot />
// Render content into the slot from anywhere
<HeaderPortal>
<button>Save</button>
</HeaderPortal>MIT
