開源日報每天推薦一個 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/