Build extensible React components with slot-based architecture. Define extension points where plugins and third-party code can inject content.
Slots are named extension points in a component where content can be injected from outside.
Vue example:
<!-- Sidebar.vue -->
<aside>
<nav>Core navigation</nav>
<slot name="widgets"></slot>
</aside>
<!-- Usage -->
<Sidebar>
<template #widgets>
<AnalyticsWidget />
<UserStatsWidget />
</template>
</Sidebar>React doesn't have a built-in slot system. This creates challenges when building extensible architectures where different parts of your app (or plugins) need to inject content into predefined locations.
You're building an admin dashboard. Plugins should be able to add widgets to the sidebar without modifying the core Sidebar component:
// Sidebar.tsx - core component (shouldn't change when plugins are added)
export const Sidebar = () => (
<aside>
<nav>Core navigation</nav>
{/* π€ How do plugins inject widgets here? */}
</aside>
);
// plugin-analytics/index.ts - separate package
// This plugin wants to add analytics widget to sidebar
// How??? π€·ββοΈ- Collecting everything in parent component - tight coupling, parent must know all plugins
- Context with manual management - lots of boilerplate per extension point
- Passing render functions through props - verbose, non-intuitive API
With @grlt-hub/react-slots, define extension points once and inject components from anywhere:
// Sidebar.tsx - define the slot
import { createSlots, createSlotIdentifier } from '@grlt-hub/react-slots';
const { slotsApi, Slots } = createSlots({
Widgets: createSlotIdentifier(),
} as const);
const Sidebar = () => (
<aside>
<nav>Core navigation</nav>
<Slots.Widgets /> {/* Extension point */}
</aside>
);
// plugin-analytics/index.ts - inject from anywhere!
slotsApi.Widgets.insert({
Component: () => <AnalyticsWidget />,
});
// plugin-user-stats/index.ts - another plugin
slotsApi.Widgets.insert({
Component: () => <UserStatsWidget />,
});
// Result:
// <aside>
// <nav>Core navigation</nav>
// <AnalyticsWidget />
// <UserStatsWidget />
// </aside>No props drilling, no boilerplate - just define slots and inject content from anywhere in your codebase.
npm i @grlt-hub/react-slots
# or
pnpm add @grlt-hub/react-slots
# or
bun add @grlt-hub/react-slots
# or
yarn add @grlt-hub/react-slotsNote: TypeScript types are included out of the box.
react^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0effector23effector-react23nanoid*
Here's a minimal working example:
import { createSlots, createSlotIdentifier } from '@grlt-hub/react-slots';
// 1. Create slots
const { slotsApi, Slots } = createSlots({
Footer: createSlotIdentifier(),
} as const);
// 2. Use slot in your component
const App = () => (
<div>
<h1>My App</h1>
<Slots.Footer />
</div>
);
// 3. Insert content into the slot
slotsApi.Footer.insert({
Component: () => <p>Β© 1955β1985β2015 Outatime Corp.</p>,
});
// Result:
// <div>
// <h1>My App</h1>
// <p>Β© 1955β1985β2015 Outatime Corp.</p>
// </div>// Define slot with typed props
const { slotsApi, Slots } = createSlots({
UserPanel: createSlotIdentifier<{ userId: number }>(),
} as const);
// Use in component
<Slots.UserPanel userId={123} />;
// Insert component - receives props automatically
slotsApi.UserPanel.insert({
Component: (props) => <UserWidget id={props.userId} />,
});const { slotsApi, Slots } = createSlots({
UserPanel: createSlotIdentifier<{ userId: number }>(),
} as const);
<Slots.UserPanel userId={123} />;
slotsApi.UserPanel.insert({
// Transform userId into userName and isAdmin before passing to component
mapProps: (slotProps) => ({
userName: getUserName(slotProps.userId),
isAdmin: checkAdmin(slotProps.userId),
}),
Component: (props) => <UserBadge name={props.userName} admin={props.isAdmin} />,
});Components are inserted in any order, but rendered according to order value (lower numbers first):
// This is inserted first, but will render second
slotsApi.Sidebar.insert({
Component: () => <SecondWidget />,
order: 2,
});
// This is inserted second, but will render first
slotsApi.Sidebar.insert({
Component: () => <FirstWidget />,
order: 1,
});
// Result:
// <>
// <FirstWidget /> β order: 1
// <SecondWidget /> β order: 2
// </>Note: Components with the same order value keep their insertion order and all of them are rendered.
Remove all components from a slot:
// Insert components
slotsApi.Sidebar.insert({
Component: () => <Widget1 />,
});
slotsApi.Sidebar.insert({
Component: () => <Widget2 />,
});
// Result after inserts:
// <aside>
// <Widget1 />
// <Widget2 />
// </aside>
// Later, clear the slot
slotsApi.Sidebar.clear();
// Result after clear:
// <aside>
// {/* Sidebar slot is now empty */}
// </aside>Wait for data to load before inserting component. The component won't render until the event fires:
import { createEvent } from 'effector';
const userLoaded = createEvent<{ id: number; name: string }>();
// Component will be inserted only after userLoaded fires
slotsApi.Header.insert({
when: userLoaded,
mapProps: (slotProps, whenPayload) => ({
userId: whenPayload.id,
userName: whenPayload.name,
}),
Component: (props) => <UserWidget id={props.userId} name={props.userName} />,
});
// Result before userLoaded fires:
// <header>
// {/* Header slot is empty, waiting... */}
// </header>
// Later, when data arrives:
userLoaded({ id: 123, name: 'John' });
// Result after userLoaded fires:
// <header>
// <UserWidget id={123} name="John" />
// </header>Note: You can pass an array of events when: [event1, event2] - component inserts when any of them fires. Use once from patronum if you need one-time insertion.
