
Within our applications, we often use the compound components pattern to build rich widgets while keeping a compositional and readable API.
In this article, I’ll outline the criteria, examples, and best practices adopted in the project for those who need to write or extend complex components without sacrificing developer experience.
Why choose compound components
Pattern anatomy
1// apps/app/components/QuoteMobileCard.tsx2const QuoteMobileCardContext = createContext<QuoteMobileCardContextProps | undefined>(undefined);34const useQuoteMobileCardContext = () => {5 const context = useContext(QuoteMobileCardContext);67 if (!context) {8 throw new Error('QuoteMobileCard compound components must be used within a QuoteMobileCard provider');9 }1011 return context;12};
1// apps/app/components/QuoteMobileCard.tsx2function QuoteMobileCardRoot({3 children,4 isLoading = false,5 onClick,6}: QuoteMobileCardRootProps) {7 return (8 <QuoteMobileCardContext.Provider value={{ isLoading }}>9 <Stack10 component={ButtonBase}11 onClick={onClick}12 >13 {children}14 </Stack>15 </QuoteMobileCardContext.Provider>16 );17}
1// apps/app/components/QuoteMobileCard.tsx2const QuoteMobileCardTitle = ({ children }: { readonly children: React.ReactNode }) => {3 const { isLoading } = useQuoteMobileCardContext();4 return isLoading5 ? <Skeleton variant="text" width="40%" />6 : <Typography variant="h6">{children}</Typography>;7};
1// apps/app/components/QuoteMobileCard.tsx2export const QuoteMobileCard = {3 Root: QuoteMobileCardRoot,4 Main: QuoteMobileCardMain,5 Title: QuoteMobileCardTitle,6 Subtitle: QuoteMobileCardSubtitle,7};
Below is a practical example of optional composition built using the same building blocks described above.
Each block can be included or omitted depending on the current view, while maintaining a declarative API.
1<QuoteMobileCard.Root isLoading={isLoading}>2 <QuoteMobileCard.Title>3 {quoteTitle}4 </QuoteMobileCard.Title>56 {showSubtitle && (7 <QuoteMobileCard.Subtitle>8 Aggiornato il {lastUpdate}9 </QuoteMobileCard.Subtitle>10 )}1112 <QuoteMobileCard.Main13 name={storeCode}14 infoString={description}15 />16</QuoteMobileCard.Root>
To introduce the optional element QuoteMobileCard.Subtitle, simply define a new sub-component that reads from the same context and handles its own fallback during the loading phase.
1const QuoteMobileCardTitle = ({ children }: { readonly children: React.ReactNode }) => {2 const { isLoading } = useQuoteMobileCardContext();3 return isLoading4 ? <Skeleton variant="text" width="40%" sx={{ fontSize: '1rem' }} />5 : <Typography variant="h6">{children}</Typography>;6};78const QuoteMobileCardSubtitle = ({ children }: { readonly children: React.ReactNode }) => {9 const { isLoading } = useQuoteMobileCardContext();10 return isLoading11 ? <Skeleton variant="text" width="60%" sx={{ fontSize: '0.75rem' }} />12 : <Typography variant="body2" color="text.secondary">{children}</Typography>;13};1415export const QuoteMobileCard = {16 Root: QuoteMobileCardRoot,17 Main: QuoteMobileCardMain,18 Title: QuoteMobileCardTitle,19 Subtitle: QuoteMobileCardSubtitle,20};
This way, the developer using the component decides which blocks to mount, without propagating additional props or scattering conditional logic across the layout.
Best practices we maintain
How to build a new compound component
The compound components pattern allows us to keep complex layouts consistent across the entire application, reducing prop drilling and improving readability.
By following the steps outlined above, we can introduce new composite widgets while maintaining the same developer experience (DX).