There are a lot of ways to do the job of building software and somehow even more ways to building user interfaces. You can write code in lots of different ways, with different priorities, paradigms and, frankly, taste and it will end up working. You often don't feel the pain or debt of these decisions until you need to build on top of it or the context of where or how it's used changes. Here are some thoughts on patterns and taste that I’ve found to create maintainable code that’s easy to improve, reuse and collaborate with others on. ## Children Almost all ways to build user interfaces are tree-based. An element can be a child and it can have children. I think this is one of the greatest things about the UI abstractions that we’ve settled on. When building components, in this case in React, sometimes it’s easy to forget this and end up with a configuration based approach to defining behaviour and content. Take this example: ```tsx // Prop-based interface NavigationItem { type: 'link' | 'button'; text: string; icon?: ReactNode; to?: string; onClick?: () => void; } interface HeaderProps { navigation: NavigationItem[]; logo?: string; account?: { name: string; email: string; }; } export const Header: FC<HeaderProps> = ({ navigation, logo, account }) => { return ( <header className="flex justify-between py-3 border-b"> <div className="flex gap-3"> {logo && <img src={logo} alt="logo" className="h-8" />} {navigation.length > 0 && ( <div className="flex gap-3"> {navigation.map((item, index) => ( <div key={index}> {item.type === 'link' ? ( <a className="px-3 py-2 bg-gray-50 hover:bg-gray-100" href={item.to}> {item.text} </a> ) : ( <button className="px-3 py-2 bg-gray-50 hover:bg-gray-100" onClick={item.onClick}> {item.text} </button> )} </div> ))} </div> )} </div> {account && ( <div> <p>{account.name}</p> <p className="text-sm">{account.email}</p> </div> )} </header> ); }; ``` ```tsx // Prop-based export const Layout: FC = () => { return ( <main> <Header logo="/logo.png" navigation={[ { type: 'link', text: 'Home', to: '/' }, { type: 'button', text: 'About', onClick: () => { alert('About clicked'); } } ]} account={{ name: 'John Doe', email: '[email protected]' }} /> </main> ); }; ``` This is a totally plausible implementation of a pretty decent header. The problem here is you’re essentially having to model the behaviour of everything that could be in the header as a data structure outside of the UI that you’re creating. Even in this example, a navigation item can be a link or a button with different properties leading to conditional logic. What if I wanted to add a different kind of navigation item with a pop up for more options? Change the styling of one of the items? Include a search bar which requires state & complex configuration in its own right? You’d need to model all these possible options in the same data structure which leads to, in my opinion, bad API design. These could be: * flags i.e `isPopup`, `showFeature`, etc. This makes this main component balloon in size & complexity. * leaky element properties, i.e passing in `className`, `style`, `onClick`, `color`, etc. When you’re including specific UI properties alongside data, you’re then dealing with a very fuzzy, incomplete recreation of the base UI platform. * leaky functionality implementation, i.e `searchQuery`, `onQueryChange`, `searchResults`. Why does the header need to know about the search apart from the fact that it happens to contain it? What would it look like if we lent into the tree paradigm more? If we optimised for composition over configuration? ```tsx // Composition-based export const Header: FC<PropsWithChildren> = ({ children }) => { return <header className="flex justify-between py-3 border-b">{children}</header>; }; export const Link: FC<PropsWithChildren<AnchorHTMLAttributes<HTMLAnchorElement>>> = ({ children, className, ...props }) => { return ( <a className={cn('px-3 py-2 bg-gray-50 hover:bg-gray-100', className)} {...props}> {children} </a> ); }; export const Button: FC<PropsWithChildren<HTMLAttributes<HTMLButtonElement>>> = ({ children, className, ...props }) => { return ( <button className={cn('px-3 py-2 bg-gray-50 hover:bg-gray-100', className)} {...props}> {children} </button> ); }; ``` ```tsx // Composition-based export const Layout: FC = () => { return ( <main> <Header> <img src="/logo.png" alt="logo" className="h-8" /> <div className="flex gap-3"> <Link href="/home">Home</Link> <Button onClick={() => alert('About clicked')}>About</Button> </div> <div> <p>John Doe</p> <p className="text-sm">[email protected]</p> </div> </Header> </main> ); }; ``` For me, this is the best balance of composition & abstraction. Note that this isn’t the same as copy & pasting the “html” for another use case. If I change the styling or functionality of the `<Header />`, `<Link />` or `<Button/>` it will be represented everywhere a header is needed to be shown. What it does preserve is being unopinionated about how it’s being used and composed. I'd always recommend trying to think about how much your components really need to know about the children they are rendering. If they don't need to know anything, like this example, always try and reach for composition over configuration. It also has the benefit of having extremely little complexity and very little implementation. This leads it to be easily maintained and understood by other developers on the project (and you in the future). It simply follows the same rule everything else does: it can be a child and it can have children.