A powerful and extensible utility for selecting and manipulating React components based on attributes. Ideal for dynamic layouts, slotted rendering, and component injection patterns.
- react-cmp-selector
npm install react-cmp-selector
or using yarn:
yarn add react-cmp-selector
React does not support named slots or dynamic selection of children out of the box. This utility solves that by providing:
- A hook to search through children by attribute
- A component-based API (
<Slot>
) for declarative slot usage - Tools to inject props, merge handlers, and validate layout contracts
Feature | Prop / Option | Type | Description |
---|---|---|---|
Attribute Matching | attribute |
string |
Attribute name to search for (default: 'data-slot' ) |
Value Matching | value / name |
string |
The value to match against the selected attribute |
Prop Merging | props |
Partial<P> |
Injected props to merge into the matched component(s) |
Function Merging Strategy | functionPropMerge |
'combine' | 'override' |
Defines how function props like onClick should be merged |
Debug Mode | debug |
boolean |
Enables logging of matching and merging behavior |
Match All | findAll |
boolean |
If true, returns all matching components instead of the first |
Hook Interface | getCmpByAttr() |
— | Programmatic interface to extract and modify children |
Declarative API | <Slot> |
— | React component alternative to the hook |
Slot Markers | SlotUtils.createMarker() |
— | Creates a named slot wrapper component |
Slot Validation | SlotUtils.validate() |
— | Dev-only validation for required slot presence |
Fallback Rendering | fallback |
ReactNode |
Rendered if no matching slot is found |
onFound Callback | onFound |
(element: ReactElement) => void |
Runs when a match is found (e.g. for side effects) |
Children
export function ChildComponents() {
return (
<>
<div data-slot="header">Header</div>
<div data-slot="body">Body</div>
<div data-slot="footer">Footer</div>
</>
);
}
Parent
const header = getCmpByAttr({
children: <ChildComponents />,
value: "header",
props: { className: "highlighted" },
});
Output
<div data-slot="header" class="highlighted">Header</div>
How It Works
getCmpByAttr()
searches children fordata-slot="header"
.- The match is cloned with the
className
prop added.
Children
function Buttons() {
return (
<>
<button data-role="action-button">Save</button>
<button data-role="action-button">Cancel</button>
</>
);
}
Parent
const buttons = getCmpByAttr({
children: <Buttons />,
attribute: "data-role",
value: "action-button",
findAll: true,
props: { "data-tracked": true },
});
Output
<button data-role="action-button" data-tracked="true">Save</button>
<button data-role="action-button" data-tracked="true">Cancel</button>
Children
function CTA() {
return (
<button data-slot="cta" onClick={() => console.log("child")}>
Click Me
</button>
);
}
Parent
const cta = getCmpByAttr({
children: <CTA />,
value: "cta",
props: {
onClick: () => console.log("parent"),
},
functionPropMerge: "combine",
});
Behavior
Console logs:
child
parent
export function Layout({ children }: { children: React.ReactNode }) {
return (
<div>
<Slot name="header">{children}</Slot>
<main>
<Slot name="content">{children}</Slot>
</main>
</div>
);
}
function PageContent() {
return (
<>
<div data-slot="header">Welcome</div>
<div data-slot="content">Hello, world!</div>
</>
);
}
<Layout>
<PageContent />
</Layout>;
Output
<div>
<div data-slot="header">Welcome</div>
<main>
<div data-slot="content">Hello, world!</div>
</main>
</div>
<Slot name="hero" fallback={<div>Default Hero</div>}>
{children}
</Slot>
If no data-slot="hero"
is found, it renders:
<div>Default Hero</div>
Marker Declaration
const HeroSlot = SlotUtils.createMarker("hero");
function Page() {
return (
<HeroSlot>
<div className="hero-banner">Custom Hero</div>
</HeroSlot>
);
}
Parent
<Slot name="hero" fallback={<div>Default Hero</div>}>
<Page />
</Slot>
SlotUtils.validate(children, ["header", "footer"]);
- Dev-only.
- Warns if
data-slot="header"
orfooter
is missing in children.
- Composable Layouts: Dynamically slot content into shared layouts.
- Design Systems: Enable flexible API layers with predictable slot names.
- Multi-brand / White-label UIs: Inject branding-specific content without hardcoding.
- Next.js Layouts: Use context + slots to bridge
app/layout.tsx
and pages. - Dynamic Prop Injection: Apply analytics, A/B testing, or class injection to specific slots.
function getCmpByAttr<P>(
options: ComponentFinderProps<P>
): ReactNode | ReactNode[] | null;
interface ComponentFinderProps<P = unknown> {
children: ReactNode;
attribute?: string;
value?: string;
props?: Partial<P>;
debug?: boolean;
findAll?: boolean;
onFound?: (component: ReactElement) => void;
functionPropMerge?: "combine" | "override";
}
<Slot
name="footer"
props={{ className: "sticky" }}
fallback={<DefaultFooter />}
>
{children}
</Slot>
SlotUtils.createMarker(name: string, attribute?: string): Component
SlotUtils.validate(children: ReactNode, requiredSlots: string[], attribute?: string): void
- Next.js layouts require a shared context if crossing page boundaries.
getCmpByAttr
only works on elements rendered within the same render cycle.- This is not a DOM query tool – it’s entirely based on React element trees.
MIT License