useSlot for Compound Components
The useSlot
hook is a powerful tool for creating Compound Components declaratively and organizedly. It allows grouping several related subcomponents under a main component, resulting in more expressive and easier-to-use APIs.
The Problem: Managing Complex Components
Section titled “The Problem: Managing Complex Components”Often, we need to build UI components composed of several distinct but logically connected parts. Think of a Card
(with Card.Header
, Card.Body
, Card.Footer
) or an application Header
(with Left
, Center
, Right
sections).
Traditionally, implementing this could involve:
- Exporting multiple components separately (
Header
,HeaderLeft
,HeaderRight
, …), which can pollute the namespace. - Using
React.Context
for implicit communication between parts, which can add complexity. - Passing components as props, which can make the usage API less intuitive.
The Solution: useSlot
for Compound Components
Section titled “The Solution: useSlot for Compound Components”useSlot
offers a different and elegant approach. You define a configuration object where the keys represent the “slots” (the parts of your compound component) and the values are the React components (or styled components with useStyled
) that should render in each slot.
useSlot
then returns a main component that has the slot components attached as static properties. This allows for intuitive syntax like MyComponent.SlotA
or MyComponent.SlotB.SubSlot
.
Complete Example: A Card
Component
Section titled “Complete Example: A Card Component”Let’s see how to create a Card
component composed of a Header
(with Title
), Body
, and Footer
using useStyled
for styling and useSlot
for the compound structure.
1. Define Base/Styled Components:
First, we create the components that will represent each visual part of our Card using useStyled
.
import React from 'react';import { useStyled, useSlot } from 'use-styled';import { View, Text } from 'react-native'; // Or 'div', 'h2', 'p' for web
// --- Base Component Definitions ---
// The main Card containerconst CardRoot = useStyled(View, { base: { className: 'border border-gray-300 rounded-lg shadow-md overflow-hidden bg-white', // style: { borderColor: '#ccc', ... } // RN style }, variants: { shadowSize: { sm: { className: 'shadow-sm' }, md: { className: 'shadow-md' }, lg: { className: 'shadow-lg' }, } }, defaultVariants: { shadowSize: 'md', }});
// The Header sectionconst CardHeader = useStyled(View, { base: { className: 'px-4 py-3 border-b border-gray-200 bg-gray-50', },});
// The title within the Headerconst CardTitle = useStyled(Text, { base: { className: 'text-lg font-semibold text-gray-800', },});
// The main body of the Cardconst CardBody = useStyled(View, { base: { className: 'p-4', },});
// Default text within the Card bodyconst CardBodyText = useStyled(Text, { base: { className: 'text-gray-700', // Default style for body text }, variants: { muted: { true: { className: 'text-gray-500 text-sm' } // Optional variant for 'muted' text } }});
// The Card footerconst CardFooter = useStyled(View, { base: { className: 'px-4 py-3 border-t border-gray-200 bg-gray-50', },});
2. Create the Card
Compound Component with useSlot
:
We group the components defined above into their corresponding slots using useSlot
.
// --- Compound Component Creation ---
// Slot for the Header and its sub-slotsexport const CardHeaderSlot = useSlot(CardHeader, { // Component for <Card.Header> Title: CardTitle, // Component for <Card.Header.Title>});
// Slot for the Body and its sub-slotsexport const CardBodySlot = useSlot(CardBody, { // Component for <Card.Body> Text: CardBodyText, // Component for <Card.Body.Text>});
// Main slot for the Card, using the nested slotsexport const Card = useSlot(CardRoot, { // Component for <Card> Header: CardHeaderSlot, // Component for <Card.Header> Body: CardBodySlot, // Component for <Card.Body> Footer: CardFooter, // Component for <Card.Footer>});
3. Use the Card
Compound Component:
Usage becomes very declarative and intuitive.
// --- Using the Card Component ---
function MyComponent() { return ( <Card shadowSize="lg"> {/* Props are passed to Card.Root */}
<Card.Header> {/* Props can be passed to Card.Header (CardHeader) */} <Card.Header.Title> {/* Props can be passed to Card.Header.Title (CardTitle) */} Card Title </Card.Header.Title> </Card.Header>
<Card.Body> {/* Using the Text sub-slot */} <Card.Body.Text> This is the main content of the card. </Card.Body.Text> <Card.Body.Text muted={true} className="mt-2"> This is secondary information. </Card.Body.Text> </Card.Body>
<Card.Footer className="flex-row justify-end"> {/* Props are passed to Card.Footer */} <button className="text-blue-600 hover:underline"> Action in Footer </button> </Card.Footer>
</Card> );}
This example demonstrates how useSlot
allows building a clean and structured component API, where each part of the Card
is accessible via static properties, while styling and internal logic are encapsulated by the base components defined with useStyled
.
Benefits
Section titled “Benefits”- Intuitive Usage API: The
Component.Part.SubPart
syntax for using the component remains clear and declarative. - Modular and Organized Definitions: Each
useSlot
call defines a specific component and its direct sub-slots, leading to more focused and manageable code. Deeply nested structures are built by composing these modular slot definitions. - Enhanced Reusability: Not only can base styled components be reused, but the defined slot components (like
CardHeaderSlot
) can also be potentially reused in other compound components. - Explicit Composition: The new structure makes the composition of complex components more explicit and easier to follow, as nested slots are built by combining individual
useSlot
results. - Strong Typing Capabilities: The focused nature of each
useSlot
call can facilitate even more precise TypeScript typings for each component and its associated slots.
useSlot
offers a powerful and declarative way to build complex UIs and keep your component APIs clean and easy to understand.