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 ---
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
.
Benefits
Section titled “Benefits”- 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.