React
从 `div` 到 `Flex` 理解 React 小组件的价值
这篇笔记记录一下我刚开始写 React 时,对组件封装逐渐改变的理解。 我一开始写页面,总是在业务组件里直接写很多 div: 这样写当然没有问题。HTML 能正常渲染,CSS 也能实现想要的效果。 但是写多了以后,总感觉哪里不太对。 每次看业务代码,都要在一堆 div、className 和样式之间来回判断: 这个 d

这篇笔记记录一下我刚开始写 React 时,对组件封装逐渐改变的理解。
我一开始写页面,总是在业务组件里直接写很多 div:
function UserCard() {
return (
<div className="user-card">
<div className="user-card-header">
<div className="user-card-title">用户信息</div>
<div className="user-card-actions">
<button>编辑</button>
<button>删除</button>
</div>
</div>
<div className="user-card-content">
<div>姓名:小明</div>
<div>状态:正常</div>
</div>
</div>
);
}这样写当然没有问题。HTML 能正常渲染,CSS 也能实现想要的效果。
但是写多了以后,总感觉哪里不太对。
每次看业务代码,都要在一堆 div、className 和样式之间来回判断:
- 这个
div是用来做布局的,还是单纯包一层? - 这里是横向排列,还是纵向排列?
- 子元素之间有没有间距?
- 为什么这里需要
align-items: center? - 这个类名是不是只为了写一行
display: flex?
后来有一天,我看到别人的组件库里有一个 Flex 组件:
<Flex align="center" gap={8}>
<Button>编辑</Button>
<Button>删除</Button>
</Flex>我第一反应是:
这不就是 display: flex 吗?写一个 div 也能实现,为什么还要单独封装一个组件?
再仔细想了一下,我好像突然明白了什么。
能实现,不代表表达得好
用普通 div 写 Flex 布局:
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
}}
>
<Button>编辑</Button>
<Button>删除</Button>
</div>它能正确工作。
但是读代码时,需要先扫一遍 style,才能确认这个容器的作用。
换成:
<Flex align="center" gap={8}>
<Button>编辑</Button>
<Button>删除</Button>
</Flex>看到标签名的第一眼,就知道这是一个 Flex 布局容器。
这就是语义上的区别。
HTML 原生标签描述的是比较底层的结构:
<div />业务和组件库里的标签,可以表达更明确的意图:
<Flex />
<Stack />
<Card />
<PageHeader />
<UserStatus />它们不一定做了多复杂的事情,但能让代码更接近我们思考页面的方式。
Flex 组件到底封装了什么
一个最小版本的 Flex 组件并不复杂:
import type { CSSProperties, HTMLAttributes, ReactNode } from "react";
type FlexProps = HTMLAttributes<HTMLDivElement> & {
children?: ReactNode;
direction?: CSSProperties["flexDirection"];
align?: CSSProperties["alignItems"];
justify?: CSSProperties["justifyContent"];
gap?: CSSProperties["gap"];
wrap?: CSSProperties["flexWrap"];
};
export function Flex({
children,
direction = "row",
align,
justify,
gap,
wrap,
style,
...restProps
}: FlexProps) {
return (
<div
{...restProps}
style={{
display: "flex",
flexDirection: direction,
alignItems: align,
justifyContent: justify,
gap,
flexWrap: wrap,
...style,
}}
>
{children}
</div>
);
}使用时:
<Flex align="center" justify="space-between" gap={12}>
<span>用户信息</span>
<Button>编辑</Button>
</Flex>这个组件并没有发明新的布局能力。
底层还是:
display: flex;但是它把常用的 Flex 配置整理成了一套更容易读懂的接口。
为什么小组件也值得封装
我以前会觉得,只有比较重的组件才值得封装。
比如:
TableFormModalSelectDatePicker
这些组件有复杂交互和很多配置,封装起来很自然。
但是 Flex 看起来太简单了。它可能只有十几行代码。
后来我发现,衡量一个组件有没有价值,不能只看它内部代码有多少行。
一个小组件只要能稳定发挥作用,就有封装价值。
Flex 的价值包括:
- 提供清晰的布局语义。
- 减少重复的样式代码。
- 统一常用属性的写法。
- 降低业务组件里的视觉噪音。
- 方便以后统一调整布局规则。
- 让代码评审时更容易看懂页面结构。
组件内部很简单,不代表它没有价值。
有时候正是这些小组件,能让业务代码清爽很多。
对比一下业务代码
全部使用 div:
function UserCard() {
return (
<div className="user-card">
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<span>用户信息</span>
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
}}
>
<Button>编辑</Button>
<Button>删除</Button>
</div>
</div>
<div
style={{
display: "flex",
flexDirection: "column",
gap: 12,
marginTop: 16,
}}
>
<div>姓名:小明</div>
<div>状态:正常</div>
</div>
</div>
);
}换成布局组件:
function UserCard() {
return (
<Card>
<Flex align="center" justify="space-between">
<span>用户信息</span>
<Flex align="center" gap={8}>
<Button>编辑</Button>
<Button>删除</Button>
</Flex>
</Flex>
<Stack gap={12} style={{ marginTop: 16 }}>
<div>姓名:小明</div>
<div>状态:正常</div>
</Stack>
</Card>
);
}第二段代码不一定更短很多,但结构明显更容易读:
Card
-> 顶部是一行左右分布的内容
-> 右边是一组横向按钮
-> 下面是一组纵向信息这时 JSX 更像页面结构的说明,而不是 CSS 配置的集合。
再加一个 Stack
Flex 布局里,纵向排列也很常见。
可以直接写:
<Flex direction="column" gap={12}>
<div>姓名:小明</div>
<div>状态:正常</div>
</Flex>也可以在 Flex 基础上再封装一个更明确的 Stack:
import type { ComponentProps } from "react";
type StackProps = Omit<ComponentProps<typeof Flex>, "direction">;
export function Stack(props: StackProps) {
return <Flex direction="column" {...props} />;
}使用时:
<Stack gap={12}>
<div>姓名:小明</div>
<div>状态:正常</div>
</Stack>看到 Stack,就知道内容会纵向堆叠。
它甚至比:
<Flex direction="column" />更容易一眼看懂。
组件封装是在提高表达层级
我后来觉得,写业务组件时可以把代码分成不同层级。
比较底层的是:
<div style={{ display: "flex", gap: 8 }} />再高一层是:
<Flex gap={8} />更靠近业务的是:
<UserActions />它们都可能是正确写法,只是表达的层级不同。
比如:
function UserActions() {
return (
<Flex gap={8}>
<Button>编辑</Button>
<Button>删除</Button>
</Flex>
);
}在用户详情页面里,可以直接写:
<UserActions />读代码的人不需要先关心按钮怎么排列,只需要知道这里是用户操作区域。
组件封装的一个重要作用,就是让业务代码停留在合适的表达层级。
不是每一个 div 都要封装
理解到这里以后,也很容易走到另一个极端:
看到一个 div 就想新建一个组件。
例如:
<MarginTop16>
<FlexCenter>
<TextGray>
内容
</TextGray>
</FlexCenter>
</MarginTop16>这种封装不一定比直接写样式更好。
组件太碎以后,也会带来问题:
- 需要记忆很多组件名称。
- 想改一个简单样式时,要来回跳文件。
- 组件组合层级太深。
- 封装只是换了一个名字,没有形成稳定语义。
- 设计规则还没稳定,接口却先固定下来了。
所以我现在觉得,不是“小组件都要封装”,而是“小组件也可以封装”。
关键要看它有没有真正发挥作用。
我会怎么判断是否值得封装
可以问自己几个问题。
1. 它是不是反复出现
如果很多地方都在写:
style={{
display: "flex",
alignItems: "center",
gap: 8,
}}那就很适合提取一个 Flex。
2. 新名字能不能让代码更容易理解
比如:
<Stack />比:
<div style={{ display: "flex", flexDirection: "column" }} />更明确。
3. 它能不能形成统一规则
比如组件库规定间距只能从设计系统中选择:
<Flex gap="small" />
<Flex gap="medium" />
<Flex gap="large" />这样大家不会在页面里随手写出:
gap={13}4. 它是不是降低了业务组件的噪音
如果封装后,业务代码更容易看出页面结构,而不是被样式细节淹没,那通常值得做。
5. 它有没有隐藏掉必要能力
Flex 组件不能为了统一写法,反而让普通需求变得很难实现。
所以前面的例子保留了:
style
className
onClick
id这些原生 div 属性。
一个基础组件应该提供常用规则,也要保留合理的扩展能力。
样式属性和语义属性怎么选
最简单的 Flex 可以直接暴露 CSS 属性:
<Flex
align="center"
justify="space-between"
gap={12}
/>这种方式直观,也比较灵活。
如果项目有稳定的设计规范,还可以把属性继续收紧:
type Space = "small" | "medium" | "large";
const spaceMap: Record<Space, number> = {
small: 4,
medium: 8,
large: 16,
};然后写:
<Flex gap="medium" />这样 gap 不再是任意数字,而是设计系统里的间距名称。
哪种更好,没有绝对答案。
如果项目刚开始,规则还没稳定,可以先保持简单。
如果多人协作、页面越来越多,或者已经有明确的设计系统,就可以逐渐收紧接口。
小组件和业务组件的区别
我现在会把组件粗略分成几类。
1. 基础布局组件
主要表达通用布局规则:
<Flex />
<Stack />
<Grid />
<Container />2. 基础视觉组件
主要统一视觉和交互:
<Button />
<Card />
<Tag />
<Divider />3. 复杂通用组件
通常有比较多状态和交互:
<Table />
<Form />
<Modal />
<Select />4. 业务组件
直接表达当前系统里的业务概念:
<UserCard />
<OrderStatus />
<DeviceInfo />
<FirmwareUpgradePanel />以前我只重视第三类,觉得复杂组件才值得封装。
后来发现,第一类和第二类虽然简单,但它们决定了业务代码是不是容易阅读。第四类则让页面结构更接近业务本身。
这些层级配合起来,维护体验才会真正变好。
children 让容器组件保持灵活
React 组件可以通过 children 接收嵌套内容:
type CardProps = {
children: React.ReactNode;
};
function Card({ children }: CardProps) {
return <div className="card">{children}</div>;
}使用时:
<Card>
<Flex justify="space-between">
<span>标题</span>
<Button>编辑</Button>
</Flex>
</Card>Card 不需要提前知道内部会放什么。
它只负责提供一个稳定的视觉容器。
React 官方文档也把这种模式用在 Card、面板、网格等视觉包装组件上。children 让简单容器组件既有明确作用,又不会绑定具体业务。
可维护性不只是减少代码行数
以前我容易把“封装”理解成减少重复代码。
这个目标当然重要,但可维护性不只是代码行数更少。
有时候封装以后,总代码量甚至会多一点:
<Flex align="center" gap={8}>
...
</Flex>不一定比:
<div className="actions">
...
</div>更短。
但是它减少了理解成本。
读代码的人不需要再去 CSS 文件里确认 actions 到底做了什么,也不用每次重新分析 style。
代码不只是写给浏览器执行的,也是写给以后维护它的人看的。
我最后想通的地方
我刚开始写 React 时,总是在业务组件里堆很多 div。
后来看到 Flex,一开始觉得它只是给:
display: flex;套了一层壳,好像没有什么必要。
但是再回头看,才发现它解决的不只是样式复用问题。
它是在表达意图:
这里是一个横向或者纵向布局容器
这里的子元素按照某种规则排列这种封装很简单,但效果确实不错。
我也开始明白,不是只有 Table、Form 这些重组件才需要封装。
小组件也可以封装。
只要它能稳定表达一个概念,减少重复细节,让业务代码更容易理解,它就已经发挥了作用。
不过也不用为了封装而封装。
一个好的小组件,应该让代码更自然,而不是让简单的事情变复杂。