开源日报每天推荐一个 GitHub 优质开源项目和一篇精选英文科技或编程文章原文,坚持阅读《开源日报》,保持每日学习的好习惯。

2024年2月28日,开源日报第1119期:
今日推荐开源项目:《amis》
今日推荐英文原文:《Compound Components Pattern in React》


开源项目

今日推荐开源项目:《amis》传送门:项目链接

推荐理由:前端低代码框架,通过 JSON 配置就能生成各种页面

直达链接: baidu.github.io/amis


英文原文

今日推荐英文原文:Compound Components Pattern in React

推荐理由: React 中使用复合组件模式来构建可重用、灵活且易于扩展的组件,文章通过一个 简单的 UI 卡片的示例来示范这种方法


Compound Components Pattern in React

For instance, in this card, if you want to simply switch the position of the social action buttons, you’ll have to add some logic in the component itself but you know that it’s a very special scenario, a one-off condition, and the rest of the application is going to use the original structure of the card. But just because you need to handle this scenario as well, you’ll have to add additional logic to the component. Now imagine a big component with many such flags. Before you even realize it, the component is already bloated and difficult to comprehend. The solution to this problem is pretty straightforward.

Compound component pattern

We build a suite of reusable components and place them wherever we want based on our convenience. Hell, if you don’t want a specific part, you just remove it without adding any logic. This brings in a lot of flexibility from a devs perspective and scaling the component now becomes much easier.

The idea is to have two or more components that work together to accomplish a task.

A UI Card example using this pattern

The entire code base to this example is linked at the bottom.

import React from 'react'
import { twMerge } from 'tailwind-merge';

//tailwind classes for each component
const cardClasses = 'bg-white min-w-[320px]  rounded-lg flex flex-col items-center justify-center p-5';
const headerClasses = 'flex justify-between w-full mb-2';
const nameClasses = 'text-2xl font-bold text-center text-gray-800';
const roleClasses = 'text-md font-medium text-center text-gray-800';
const socialsClasses = 'flex items-center justify-center gap-4 my-4';
const socialButtonClasses = 'text-xl text-gray-400';
const actionsClasses = 'flex items-center justify-center w-full gap-2 mt-2'

//Individual components
const actionButtonClasses = (type) => twMerge('border-2 px-2 py-1.5 rounded text-sm font-bold w-full', type === 'primary' ? 'bg-sky-700 text-white' : 'text-gray-400 bg-white');
const Card = ({ children }) => <div className={cardClasses}> {children} </div>
const Header = ({ children }) => <div className={headerClasses}> {children} </div>
const Image = ({ src, alt }) => <img src={src} alt={alt} width={150} height={150} className='rounded-full' />
const Name = ({ children }) => <h1 className={nameClasses}>{children}</h1>
const Role = ({ children }) => <h3 className={roleClasses}>{children}</h3>
const Socials = ({ children }) => <div className={socialsClasses}> {children} </div>
const SocialButton = ({ children }) => <button className={socialButtonClasses}> {children} </button>
const Actions = ({ children }) => <div className={actionsClasses}> {children} </div>
const HeaderButton = ({ children, onClick }) =>
    <button className='text-gray-400' onClick={onClick}>
        {children}
    </button>
const ActionButton = ({ children, type, onClick }) =>
    <button className={actionButtonClasses(type)} onClick={onClick}>
        {children}
    </button>
export {
    Card, Header, ActionButton, Actions, HeaderButton, Image, Name, Role, SocialButton, Socials,
}

So this is what the card component looks like using this pattern. In here, I create all my components individually with the children prop. Whatever you pass between a tag enclosure counts as its child or children. If I use a div tag and inside I have an h1 tag and a p tag, they’ll count as this div tag’s children.

So by making use of this children prop, I give the user full control on what they want to render inside these components and how they want to do it.

Also, I’ve used some tailwind classes here to make it look pretty. You can copy everything from the codebase linked below.

Inside the App file, I’ll import all these components and structure them according to my needs. I can structure my component based on my use case. I can move them around, or simply remove them from the component without any additional logic and everything would still work as expected. Maximum flexibility.

<Card>
  <Image src={'https://images.unsplash.com/photo-1592334873219-42ca023e48ce?q=80&w=1000&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxjb2xsZWN0aW9uLXBhZ2V8M3w3NjA4Mjc3NHx8ZW58MHx8fHx8'} alt={'Profile image'} />
  <div className='mt-4 mb-2'>
    <Name>John Doe</Name>
    <Role>UX Specialist</Role>
  </div>
  <Socials>
    <SocialButton><IoLogoInstagram /></SocialButton>
    <SocialButton><IoLogoLinkedin /></SocialButton>
    <SocialButton><IoLogoTwitter /></SocialButton>
    <SocialButton><IoLogoYoutube /></SocialButton>
  </Socials>
</Card>

Tabs component using this example

In the case of a tabs component, I’ll need a state variable to keep track of the current active tab. You can pass state using props but sometimes there are chances of having a deeply nested component and drilling the prop to individual components is just ugly. We’ll use the context instead.

So first things first, let’s build the main Tabs container.

const TabsContext = createContext();
const Tabs = ({ children }) => {
    const [activeTab, setActiveTab] = useState(0);
    const changeTab = (tab) => setActiveTab(tab);
    return (
        <TabsContext.Provider value={{ activeTab, changeTab }}>
            <div className="w-[600px] rounded shadow-xl">{children}</div>
        </TabsContext.Provider>
    )
}

This component is essentially a wrapper that exposes the active tab state and the function that allows you to change the active tab. The context provider acts as the wrapper. So now all the children inside this container can access the activeTab and the setter function using the useContext hook.

Then let’s add the tab component. This is the button that’ll help you to switch between sections.

const Tab = ({ index, children }) => {
    const { activeTab, changeTab } = useContext(TabsContext);
    return (
        <div onClick={() => changeTab(index)} className={twMerge("py-2 transition tracking-wide text-center w-full bg-gray-200 cursor-pointer px-2 font-black text-gray-600", index === activeTab && 'bg-sky-700 text-gray-100')} >
            {children}
        </div>
    )
}

On clicking this button, it triggers the changeTab from the context changing the activeTab state. Any other component that’s accessing this state will be re-rendered.

And finally, the actual tab section. Based on the activeTab from the context, we display the specific tab section.

const TabPanel = ({ index, children }) => {
    const { activeTab } = useContext(TabsContext);
    return index === activeTab ? (
        <div className="bg-gray-100 flex justify-center items-center p-10 text-md font-bold tracking-wide text-gray-300">
            {children}
        </div>
    ) : null
}

Finally, we export all of this.

export { Tabs, Tab, TabPanel };

If you’d notice other third-party libraries, sometimes the way they export these so-called compound components is a little different. If I open up the hover card from radix UI, you’ll see the HoverCard.Trigger and HoverCard.Content inside the main container. Since functions in JavaScript are essentially objects, we can also apply the same pattern in our component. So instead of exporting things directly, we’ll default export the main tabs container and attach the other sub-components to this container. Nothing fancy here, just a different way of exporting that’s all.

Tabs.Tab = Tab;
Tabs.TabPanel = TabPanel;
export default Tabs;

Now inside the App file, let’s import all our tab components and use them as we please.

<Tabs>
  <div className='flex'>
    <Tabs.Tab index={0}>Tab 1</Tabs.Tab >
    <Tabs.Tab index={1}>Tab 2</Tabs.Tab >
    <Tabs.Tab index={2}>Tab 3</Tabs.Tab >
  </div>
  <Tabs.TabPanel index={0}>Tabpanel 1</Tabs.TabPanel>
  <Tabs.TabPanel index={1}>Tabpanel 2</Tabs.TabPanel>
  <Tabs.TabPanel index={2}>Tabpanel 3</Tabs.TabPanel>
</Tabs>

Conclusion

So that was a brief overview of this compound component pattern. It helps you build a solid design system that’s easier to scale and is much more flexible than the traditional way of building components. You can try it out by building a custom modal component or an accordion component using this pattern and see how it benefits you.


下载开源日报APP:https://openingsource.org/2579/
加入我们:https://openingsource.org/about/join/
关注我们:https://openingsource.org/about/love/