A React library for creating hoistable components that can render content in different parts of your application. Perfect for scenarios where you need to render UI elements like buttons, badges, or actions in a header, sidebar, or footer from anywhere in your component tree.
- π― Content Hoisting: Render content from any component to designated slots
- π Priority-based Ordering: Control the order of hoisted elements with priorities
- β‘ TypeScript Support: Fully typed with excellent TypeScript support
- πͺΆ Lightweight: Minimal bundle size with zero dependencies
- π Type Safe: Built with TypeScript from the ground up
- βοΈ React 18+ Ready: Uses modern React patterns and hooks
# npm
npm install @bmthd/lift
# yarn
yarn add @bmthd/lift
# pnpm
pnpm add @bmthd/lift
# bun
bun add @bmthd/liftimport { createHoistableComponent } from "@bmthd/lift";
// Create hoistable component instance
const { Provider, Slot, Hoist } = createHoistableComponent();
function App() {
return (
<Provider>
<Header>
<h1>My App</h1>
{/* This is where hoisted content will render */}
<Slot />
</Header>
<MainContent />
</Provider>
);
}
function Header({ children }: { children: React.ReactNode }) {
return (
<header style={{ display: "flex", justifyContent: "space-between", padding: "1rem" }}>
{children}
</header>
);
}
function MainContent() {
return (
<div>
<h2>Main Content</h2>
{/* These elements will be hoisted to the header */}
<Hoist priority={1}>
<button>Settings</button>
</Hoist>
<Hoist priority={2}>
<button>Profile</button>
</Hoist>
<p>The buttons above are rendered in the header, not here!</p>
</div>
);
}The hoisting pattern is useful when you need to render UI elements in a different location than where they're logically defined. Common use cases include:
- Header Actions: Render page-specific buttons in a global header
- Sidebar Content: Add contextual navigation or actions to a sidebar
- Footer Elements: Show page-specific footer content
- Toolbar Items: Dynamically populate toolbars based on current context
Creates a new hoistable component instance with three components:
const { Provider, Slot, Hoist } = createHoistableComponent();An object containing:
Provider: Context provider componentSlot: Component that renders hoisted contentHoist: Component that hoists its children to the slot
Provides the hoisting context to child components.
<Provider>
{/* Your app content */}
</Provider>Props:
children: React nodes to wrap with the hoisting context
Renders all hoisted content in priority order.
<Slot />Props: None
Hoists its children to be rendered in the corresponding <Slot>.
<Hoist priority={1}>
<button>Hoisted Button</button>
</Hoist>Props:
children: React nodes to hoistpriority?: Number (default: 0) - Lower numbers render first
Since createHoistableComponent() returns an object with Provider, Slot, and Hoist components, you cannot directly assign it to a variable like HeaderActions. Instead, you need to either:
Option 1: Use destructuring with renamed variables
import { createHoistableComponent } from "@bmthd/lift";
// Create components with descriptive names
const {
Provider: HeaderProvider,
Slot: HeaderSlot,
Hoist: HeaderHoist
} = createHoistableComponent();
const {
Provider: SidebarProvider,
Slot: SidebarSlot,
Hoist: SidebarHoist
} = createHoistableComponent();
function App() {
return (
<HeaderProvider>
<SidebarProvider>
<Layout>
<Header>
<HeaderSlot />
</Header>
<Sidebar>
<SidebarSlot />
</Sidebar>
<MainContent />
</Layout>
</SidebarProvider>
</HeaderProvider>
);
}
function MainContent() {
return (
<div>
<HeaderHoist priority={1}>
<button>Header Button</button>
</HeaderHoist>
<SidebarHoist>
<nav>Sidebar Navigation</nav>
</SidebarHoist>
</div>
);
}Option 2: Create separate modules and re-export
// header-actions.tsx
import { createHoistableComponent } from "@bmthd/lift";
export const { Provider, Slot, Hoist } = createHoistableComponent();
// sidebar-content.tsx
import { createHoistableComponent } from "@bmthd/lift";
export const { Provider, Slot, Hoist } = createHoistableComponent();
// App.tsx
import * as HeaderActions from "./header-actions";
import * as SidebarContent from "./sidebar-content";
function App() {
return (
<HeaderActions.Provider>
<SidebarContent.Provider>
<Layout>
<Header>
<HeaderActions.Slot />
</Header>
<Sidebar>
<SidebarContent.Slot />
</Sidebar>
<MainContent />
</Layout>
</SidebarContent.Provider>
</HeaderActions.Provider>
);
}
function MainContent() {
return (
<div>
<HeaderActions.Hoist priority={1}>
<button>Header Button</button>
</HeaderActions.Hoist>
<SidebarContent.Hoist>
<nav>Sidebar Navigation</nav>
</SidebarContent.Hoist>
</div>
);
}You can create multiple independent hoisting systems using either approach above. Each system maintains its own state and context.
Elements with lower priority values render first:
<Hoist priority={10}>Third</Hoist>
<Hoist priority={1}>First</Hoist>
<Hoist priority={5}>Second</Hoist>Elements with the same priority maintain their insertion order.
You can conditionally hoist content:
function ConditionalContent({ showButton }: { showButton: boolean }) {
return (
<div>
{showButton && (
<Hoist priority={1}>
<button>Conditional Button</button>
</Hoist>
)}
</div>
);
}Here's a more complete example showing a dashboard with dynamic header actions:
// header-actions.tsx
import { createHoistableComponent } from "@bmthd/lift";
export const { Provider, Slot, Hoist } = createHoistableComponent();
// App.tsx
import * as HeaderActions from "./header-actions";
function Dashboard() {
return (
<HeaderActions.Provider>
<Layout>
<Header />
<Router>
<Route path="/users" component={UsersPage} />
<Route path="/settings" component={SettingsPage} />
</Router>
</Layout>
</HeaderActions.Provider>
);
}
function Header() {
return (
<header className="header">
<h1>Dashboard</h1>
<div className="header-actions">
<HeaderActions.Slot />
</div>
</header>
);
}
function UsersPage() {
return (
<div>
<h2>Users</h2>
{/* These actions appear in the header */}
<HeaderActions.Hoist priority={1}>
<button>Add User</button>
</HeaderActions.Hoist>
<HeaderActions.Hoist priority={2}>
<button>Export</button>
</HeaderActions.Hoist>
{/* Page content */}
<UsersList />
</div>
);
}
function SettingsPage() {
return (
<div>
<h2>Settings</h2>
{/* Different actions for different pages */}
<HeaderActions.Hoist priority={1}>
<button>Save Settings</button>
</HeaderActions.Hoist>
<SettingsForm />
</div>
);
}The library is built with TypeScript and provides full type safety:
import { createHoistableComponent } from "@bmthd/lift";
const { Provider, Slot, Hoist } = createHoistableComponent();
// All components are properly typed
const MyComponent: React.FC = () => (
<Provider>
<Slot />
<Hoist priority={1}>
<button>Typed Button</button>
</Hoist>
</Provider>
);Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.