组件组合模式 #

一、复合组件模式 #

1.1 基本概念 #

复合组件是将多个相关组件组合在一起,共享状态和样式的模式:

jsx
import styled from 'styled-components';

const Card = styled.div`
  background: white;
  border-radius: 12px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  overflow: hidden;
`;

const CardHeader = styled.div`
  padding: 16px 24px;
  border-bottom: 1px solid #eee;
`;

const CardBody = styled.div`
  padding: 24px;
`;

const CardFooter = styled.div`
  padding: 16px 24px;
  background: #f9f9f9;
  border-top: 1px solid #eee;
`;

Card.Header = CardHeader;
Card.Body = CardBody;
Card.Footer = CardFooter;

function App() {
  return (
    <Card>
      <Card.Header>
        <h3>Card Title</h3>
      </Card.Header>
      <Card.Body>
        <p>Card content goes here.</p>
      </Card.Body>
      <Card.Footer>
        <button>Action</button>
      </Card.Footer>
    </Card>
  );
}

1.2 共享上下文的复合组件 #

jsx
import styled, { createContext, useContext } from 'styled-components';
import React, { createContext, useContext, useState } from 'react';

const TabsContext = createContext();

const TabsContainer = styled.div`
  width: 100%;
`;

const TabList = styled.div`
  display: flex;
  border-bottom: 2px solid #e0e0e0;
  margin-bottom: 16px;
`;

const Tab = styled.button`
  padding: 12px 24px;
  background: transparent;
  border: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -2px;
  cursor: pointer;
  font-size: 16px;
  color: ${props => props.$active ? '#667eea' : '#666'};
  font-weight: ${props => props.$active ? '600' : '400'};
  border-bottom-color: ${props => props.$active ? '#667eea' : 'transparent'};
  
  &:hover {
    color: #667eea;
  }
`;

const TabPanel = styled.div`
  display: ${props => props.$active ? 'block' : 'none'};
  padding: 16px 0;
`;

function Tabs({ children, defaultIndex = 0 }) {
  const [activeIndex, setActiveIndex] = useState(defaultIndex);
  
  return (
    <TabsContext.Provider value={{ activeIndex, setActiveIndex }}>
      <TabsContainer>{children}</TabsContainer>
    </TabsContext.Provider>
  );
}

function TabList({ children }) {
  const { activeIndex, setActiveIndex } = useContext(TabsContext);
  
  return (
    <div className="tab-list">
      {React.Children.map(children, (child, index) => 
        React.cloneElement(child, {
          $active: activeIndex === index,
          onClick: () => setActiveIndex(index),
        })
      )}
    </div>
  );
}

function TabPanel({ children, index }) {
  const { activeIndex } = useContext(TabsContext);
  
  return <TabPanel $active={activeIndex === index}>{children}</TabPanel>;
}

Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;

二、插槽模式 #

2.1 基本插槽 #

jsx
import styled from 'styled-components';

const Card = styled.div`
  background: white;
  border-radius: 12px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
`;

const CardImage = styled.div`
  height: 200px;
  background: #f0f0f0;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  
  img {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
`;

const CardContent = styled.div`
  padding: 24px;
`;

const CardActions = styled.div`
  padding: 16px 24px;
  border-top: 1px solid #eee;
  display: flex;
  gap: 12px;
  justify-content: flex-end;
`;

function ImageCard({ image, children, actions }) {
  return (
    <Card>
      {image && <CardImage>{image}</CardImage>}
      <CardContent>{children}</CardContent>
      {actions && <CardActions>{actions}</CardActions>}
    </Card>
  );
}

function App() {
  return (
    <ImageCard
      image={<img src="/photo.jpg" alt="Photo" />}
      actions={
        <>
          <button>Cancel</button>
          <button>Confirm</button>
        </>
      }
    >
      <h3>Card Title</h3>
      <p>Card description</p>
    </ImageCard>
  );
}

2.2 具名插槽 #

jsx
import styled from 'styled-components';

const Layout = styled.div`
  display: flex;
  flex-direction: column;
  min-height: 100vh;
`;

const Header = styled.header`
  padding: 16px 24px;
  background: #667eea;
  color: white;
`;

const Main = styled.main`
  flex: 1;
  padding: 24px;
`;

const Sidebar = styled.aside`
  width: 280px;
  padding: 24px;
  background: #f5f5f5;
`;

const Footer = styled.footer`
  padding: 16px 24px;
  background: #333;
  color: white;
`;

const ContentWithSidebar = styled.div`
  display: flex;
  flex: 1;
`;

function PageLayout({ header, sidebar, children, footer }) {
  return (
    <Layout>
      {header && <Header>{header}</Header>}
      <ContentWithSidebar>
        {sidebar && <Sidebar>{sidebar}</Sidebar>}
        <Main>{children}</Main>
      </ContentWithSidebar>
      {footer && <Footer>{footer}</Footer>}
    </Layout>
  );
}

function App() {
  return (
    <PageLayout
      header={<nav>Navigation</nav>}
      sidebar={<nav>Sidebar Menu</nav>}
      footer={<p>© 2024 My App</p>}
    >
      <h1>Page Content</h1>
    </PageLayout>
  );
}

三、样式组合模式 #

3.1 样式变体组合 #

jsx
import styled, { css } from 'styled-components';

const baseStyles = css`
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s;
`;

const variantStyles = {
  solid: css`
    background: ${props => props.$color || '#667eea'};
    color: white;
    border: none;
  `,
  outline: css`
    background: transparent;
    color: ${props => props.$color || '#667eea'};
    border: 2px solid ${props => props.$color || '#667eea'};
  `,
  ghost: css`
    background: transparent;
    color: ${props => props.$color || '#667eea'};
    border: none;
    
    &:hover {
      background: rgba(102, 126, 234, 0.1);
    }
  `,
};

const sizeStyles = {
  small: css`
    padding: 8px 16px;
    font-size: 14px;
    border-radius: 6px;
  `,
  medium: css`
    padding: 12px 24px;
    font-size: 16px;
    border-radius: 8px;
  `,
  large: css`
    padding: 16px 32px;
    font-size: 18px;
    border-radius: 10px;
  `,
};

const Button = styled.button`
  ${baseStyles}
  ${props => variantStyles[props.$variant || 'solid']}
  ${props => sizeStyles[props.$size || 'medium']}
  
  ${props => props.$fullWidth && css`
    width: 100%;
  `}
  
  ${props => props.$rounded && css`
    border-radius: 9999px;
  `}
  
  &:hover:not(:disabled) {
    opacity: 0.9;
  }
  
  &:disabled {
    opacity: 0.6;
    cursor: not-allowed;
  }
`;

function App() {
  return (
    <>
      <Button $variant="solid">Solid</Button>
      <Button $variant="outline" $color="#22c55e">Outline Green</Button>
      <Button $variant="ghost" $size="large">Ghost Large</Button>
      <Button $fullWidth $rounded>Full Width Rounded</Button>
    </>
  );
}

3.2 样式修饰符 #

jsx
import styled, { css } from 'styled-components';

const withElevation = (level = 'md') => css`
  box-shadow: ${{
    sm: '0 1px 2px rgba(0, 0, 0, 0.05)',
    md: '0 4px 6px rgba(0, 0, 0, 0.1)',
    lg: '0 10px 15px rgba(0, 0, 0, 0.1)',
    xl: '0 20px 25px rgba(0, 0, 0, 0.15)',
  }[level]};
`;

const withRounded = (size = 'md') => css`
  border-radius: ${{
    sm: '4px',
    md: '8px',
    lg: '12px',
    xl: '16px',
    full: '9999px',
  }[size]};
`;

const withPadding = (size = 'md') => css`
  padding: ${{
    sm: '8px',
    md: '16px',
    lg: '24px',
    xl: '32px',
  }[size]};
`;

const Card = styled.div`
  background: white;
  
  ${props => props.$elevated && withElevation(props.$elevation || 'md')}
  ${props => withRounded(props.$rounded || 'lg')}
  ${props => withPadding(props.$padding || 'lg')}
  
  ${props => props.$hoverable && css`
    transition: transform 0.2s, box-shadow 0.2s;
    
    &:hover {
      transform: translateY(-4px);
      box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
    }
  `}
`;

function App() {
  return (
    <>
      <Card $elevated $rounded="lg">Elevated Card</Card>
      <Card $elevated $elevation="xl" $hoverable>Hoverable Card</Card>
      <Card $rounded="full" $padding="sm">Pill Card</Card>
    </>
  );
}

四、组件通信模式 #

4.1 通过 Props 通信 #

jsx
import styled from 'styled-components';

const AccordionContainer = styled.div`
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  overflow: hidden;
`;

const AccordionItem = styled.div`
  border-bottom: 1px solid #e0e0e0;
  
  &:last-child {
    border-bottom: none;
  }
`;

const AccordionHeader = styled.button`
  width: 100%;
  padding: 16px 24px;
  background: ${props => props.$active ? '#f5f5f5' : 'white'};
  border: none;
  text-align: left;
  cursor: pointer;
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 16px;
  font-weight: 600;
  
  &:hover {
    background: #f9f9f9;
  }
`;

const AccordionContent = styled.div`
  padding: ${props => props.$active ? '16px 24px' : '0 24px'};
  max-height: ${props => props.$active ? '500px' : '0'};
  overflow: hidden;
  transition: all 0.3s ease;
`;

function Accordion({ items }) {
  const [activeIndex, setActiveIndex] = useState(null);
  
  return (
    <AccordionContainer>
      {items.map((item, index) => (
        <AccordionItem key={index}>
          <AccordionHeader
            $active={activeIndex === index}
            onClick={() => setActiveIndex(activeIndex === index ? null : index)}
          >
            {item.title}
            <span>{activeIndex === index ? '−' : '+'}</span>
          </AccordionHeader>
          <AccordionContent $active={activeIndex === index}>
            {item.content}
          </AccordionContent>
        </AccordionItem>
      ))}
    </AccordionContainer>
  );
}

4.2 通过 Context 通信 #

jsx
import styled, { ThemeContext } from 'styled-components';
import React, { createContext, useContext, useState } from 'react';

const ListContext = createContext();

const ListContainer = styled.ul`
  list-style: none;
  padding: 0;
  margin: 0;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  overflow: hidden;
`;

const ListItem = styled.li`
  padding: 12px 16px;
  background: ${props => props.$selected ? '#e6f0ff' : 'white'};
  border-bottom: 1px solid #eee;
  cursor: pointer;
  
  &:last-child {
    border-bottom: none;
  }
  
  &:hover {
    background: ${props => props.$selected ? '#e6f0ff' : '#f9f9f9'};
  }
`;

function SelectList({ children, value, onChange }) {
  return (
    <ListContext.Provider value={{ value, onChange }}>
      <ListContainer>{children}</ListContainer>
    </ListContext.Provider>
  );
}

function SelectItem({ children, itemValue }) {
  const { value, onChange } = useContext(ListContext);
  const selected = value === itemValue;
  
  return (
    <ListItem
      $selected={selected}
      onClick={() => onChange(itemValue)}
    >
      {children}
    </ListItem>
  );
}

function App() {
  const [selected, setSelected] = useState('option1');
  
  return (
    <SelectList value={selected} onChange={setSelected}>
      <SelectItem itemValue="option1">Option 1</SelectItem>
      <SelectItem itemValue="option2">Option 2</SelectItem>
      <SelectItem itemValue="option3">Option 3</SelectItem>
    </SelectList>
  );
}

五、渲染属性模式 #

5.1 函数作为子组件 #

jsx
import styled from 'styled-components';

const ToggleContainer = styled.div``;

const ToggleButton = styled.button`
  padding: 8px 16px;
  background: ${props => props.$active ? '#667eea' : '#e0e0e0'};
  color: ${props => props.$active ? 'white' : '#333'};
  border: none;
  border-radius: 4px;
  cursor: pointer;
`;

function Toggle({ children, defaultOn = false }) {
  const [on, setOn] = useState(defaultOn);
  
  return children({
    on,
    toggle: () => setOn(!on),
    setOn,
    setOff: () => setOn(false),
  });
}

function App() {
  return (
    <Toggle>
      {({ on, toggle }) => (
        <ToggleContainer>
          <ToggleButton $active={on} onClick={toggle}>
            {on ? 'ON' : 'OFF'}
          </ToggleButton>
          {on && <p>The toggle is on!</p>}
        </ToggleContainer>
      )}
    </Toggle>
  );
}

5.2 渲染属性组件 #

jsx
import styled from 'styled-components';

const PopoverContainer = styled.div`
  position: relative;
  display: inline-block;
`;

const PopoverContent = styled.div`
  position: absolute;
  top: 100%;
  left: 0;
  margin-top: 8px;
  padding: 16px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  z-index: 1000;
  min-width: 200px;
`;

function Popover({ renderTrigger, renderContent }) {
  const [open, setOpen] = useState(false);
  
  return (
    <PopoverContainer>
      {renderTrigger({ onClick: () => setOpen(!open) })}
      {open && (
        <>
          <div onClick={() => setOpen(false)} style={{ position: 'fixed', inset: 0 }} />
          <PopoverContent>
            {renderContent({ close: () => setOpen(false) })}
          </PopoverContent>
        </>
      )}
    </PopoverContainer>
  );
}

function App() {
  return (
    <Popover
      renderTrigger={({ onClick }) => (
        <button onClick={onClick}>Open Popover</button>
      )}
      renderContent={({ close }) => (
        <div>
          <h4>Popover Title</h4>
          <p>Popover content here</p>
          <button onClick={close}>Close</button>
        </div>
      )}
    />
  );
}

六、高阶组件模式 #

6.1 样式增强器 #

jsx
import styled, { css } from 'styled-components';

function withStyle(styles) {
  return (Component) => {
    const StyledComponent = styled(Component)`
      ${styles}
    `;
    StyledComponent.displayName = `withStyle(${Component.displayName || Component.name})`;
    return StyledComponent;
  };
}

const withCard = withStyle(css`
  padding: 24px;
  background: white;
  border-radius: 12px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
`);

const withElevation = (level = 'md') => withStyle(css`
  box-shadow: ${{
    sm: '0 1px 2px rgba(0, 0, 0, 0.05)',
    md: '0 4px 6px rgba(0, 0, 0, 0.1)',
    lg: '0 10px 15px rgba(0, 0, 0, 0.1)',
  }[level]};
`);

const BaseDiv = styled.div``;

const Card = withCard(BaseDiv);
const ElevatedCard = withElevation('lg')(Card);

6.2 组合高阶组件 #

jsx
import styled, { css } from 'styled-components';

function compose(...funcs) {
  return funcs.reduce((a, b) => (...args) => a(b(...args)), arg => arg);
}

const withPadding = (size) => withStyle(css`
  padding: ${size};
`);

const withMargin = (size) => withStyle(css`
  margin: ${size};
`);

const withBorder = (width, color) => withStyle(css`
  border: ${width} solid ${color};
`);

const withRounded = (radius) => withStyle(css`
  border-radius: ${radius};
`);

const EnhancedBox = compose(
  withPadding('24px'),
  withMargin('16px'),
  withBorder('1px', '#e0e0e0'),
  withRounded('12px')
)(styled.div``);

七、总结 #

组件组合模式速查表:

模式 用途 示例
复合组件 共享状态的组件组 Card.Header, Card.Body
插槽模式 灵活的内容分发 props.children, 具名插槽
样式组合 组合多个样式变体 variantStyles + sizeStyles
Context 通信 跨组件状态共享 ListContext.Provider
渲染属性 灵活的渲染控制 children({ on, toggle })
高阶组件 样式增强 withCard(Component)

下一步:学习 响应式设计 掌握媒体查询和响应式布局技巧。

最后更新:2026-03-28