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 ---
export const Card = useSlot({
Root: CardRoot, // Component for <Card>
Header: {
Root: CardHeader, // Component for <Card.Header>
Title: CardTitle, // Component for <Card.Header.Title>
},
Body: { // Body is now an object with sub-slots
Root: CardBody, // Component for <Card.Body>
Text: CardBodyText, // Component for <Card.Body.Text>
},
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.

  • Explicit and Intuitive API: The Component.Part syntax makes it obvious how to use the different sections of the component.
  • Organization: Keeps the definition of all related parts in one place (useSlot).
  • Reusability: Base components (RootFrame, etc.) can be reused elsewhere if needed.
  • Clear Composition: Facilitates visualizing and composing the component structure.
  • Typing (Potential): Opens doors for strongly typing the expected children structure and props for each slot.

useSlot offers a powerful and declarative way to build complex UIs and keep your component APIs clean and easy to understand.