Skip to content

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.

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.

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 container
const 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 section
const CardHeader = useStyled(View, {
base: {
className: 'px-4 py-3 border-b border-gray-200 bg-gray-50',
},
});
// The title within the Header
const CardTitle = useStyled(Text, {
base: {
className: 'text-lg font-semibold text-gray-800',
},
});
// The main body of the Card
const CardBody = useStyled(View, {
base: {
className: 'p-4',
},
});
// Default text within the Card body
const 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 footer
const 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-slots
export const CardHeaderSlot = useSlot(CardHeader, { // Component for <Card.Header>
Title: CardTitle, // Component for <Card.Header.Title>
});
// Slot for the Body and its sub-slots
export const CardBodySlot = useSlot(CardBody, { // Component for <Card.Body>
Text: CardBodyText, // Component for <Card.Body.Text>
});
// Main slot for the Card, using the nested slots
export 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.

  • 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.