组件组合模式 #
一、复合组件模式 #
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