React

从 `div` 到 `Flex` 理解 React 小组件的价值

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

2026年6月12日14 分钟阅读
从 `div` 到 `Flex` 理解 React 小组件的价值

这篇笔记记录一下我刚开始写 React 时,对组件封装逐渐改变的理解。

我一开始写页面,总是在业务组件里直接写很多 div

tsx
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 也能实现想要的效果。

但是写多了以后,总感觉哪里不太对。

每次看业务代码,都要在一堆 divclassName 和样式之间来回判断:

  • 这个 div 是用来做布局的,还是单纯包一层?
  • 这里是横向排列,还是纵向排列?
  • 子元素之间有没有间距?
  • 为什么这里需要 align-items: center
  • 这个类名是不是只为了写一行 display: flex

后来有一天,我看到别人的组件库里有一个 Flex 组件:

tsx
<Flex align="center" gap={8}>
  <Button>编辑</Button>
  <Button>删除</Button>
</Flex>

我第一反应是:

这不就是 display: flex 吗?写一个 div 也能实现,为什么还要单独封装一个组件?

再仔细想了一下,我好像突然明白了什么。

能实现,不代表表达得好

用普通 div 写 Flex 布局:

tsx
<div
  style={{
    display: "flex",
    alignItems: "center",
    gap: 8,
  }}
>
  <Button>编辑</Button>
  <Button>删除</Button>
</div>

它能正确工作。

但是读代码时,需要先扫一遍 style,才能确认这个容器的作用。

换成:

tsx
<Flex align="center" gap={8}>
  <Button>编辑</Button>
  <Button>删除</Button>
</Flex>

看到标签名的第一眼,就知道这是一个 Flex 布局容器。

这就是语义上的区别。

HTML 原生标签描述的是比较底层的结构:

tsx
<div />

业务和组件库里的标签,可以表达更明确的意图:

tsx
<Flex />
<Stack />
<Card />
<PageHeader />
<UserStatus />

它们不一定做了多复杂的事情,但能让代码更接近我们思考页面的方式。

Flex 组件到底封装了什么

一个最小版本的 Flex 组件并不复杂:

tsx
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>
  );
}

使用时:

tsx
<Flex align="center" justify="space-between" gap={12}>
  <span>用户信息</span>
  <Button>编辑</Button>
</Flex>

这个组件并没有发明新的布局能力。

底层还是:

css
display: flex;

但是它把常用的 Flex 配置整理成了一套更容易读懂的接口。

为什么小组件也值得封装

我以前会觉得,只有比较重的组件才值得封装。

比如:

  • Table
  • Form
  • Modal
  • Select
  • DatePicker

这些组件有复杂交互和很多配置,封装起来很自然。

但是 Flex 看起来太简单了。它可能只有十几行代码。

后来我发现,衡量一个组件有没有价值,不能只看它内部代码有多少行。

一个小组件只要能稳定发挥作用,就有封装价值。

Flex 的价值包括:

  • 提供清晰的布局语义。
  • 减少重复的样式代码。
  • 统一常用属性的写法。
  • 降低业务组件里的视觉噪音。
  • 方便以后统一调整布局规则。
  • 让代码评审时更容易看懂页面结构。

组件内部很简单,不代表它没有价值。

有时候正是这些小组件,能让业务代码清爽很多。

对比一下业务代码

全部使用 div

tsx
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>
  );
}

换成布局组件:

tsx
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>
  );
}

第二段代码不一定更短很多,但结构明显更容易读:

text
Card
  -> 顶部是一行左右分布的内容
  -> 右边是一组横向按钮
  -> 下面是一组纵向信息

这时 JSX 更像页面结构的说明,而不是 CSS 配置的集合。

再加一个 Stack

Flex 布局里,纵向排列也很常见。

可以直接写:

tsx
<Flex direction="column" gap={12}>
  <div>姓名:小明</div>
  <div>状态:正常</div>
</Flex>

也可以在 Flex 基础上再封装一个更明确的 Stack

tsx
import type { ComponentProps } from "react";

type StackProps = Omit<ComponentProps<typeof Flex>, "direction">;

export function Stack(props: StackProps) {
  return <Flex direction="column" {...props} />;
}

使用时:

tsx
<Stack gap={12}>
  <div>姓名:小明</div>
  <div>状态:正常</div>
</Stack>

看到 Stack,就知道内容会纵向堆叠。

它甚至比:

tsx
<Flex direction="column" />

更容易一眼看懂。

组件封装是在提高表达层级

我后来觉得,写业务组件时可以把代码分成不同层级。

比较底层的是:

tsx
<div style={{ display: "flex", gap: 8 }} />

再高一层是:

tsx
<Flex gap={8} />

更靠近业务的是:

tsx
<UserActions />

它们都可能是正确写法,只是表达的层级不同。

比如:

tsx
function UserActions() {
  return (
    <Flex gap={8}>
      <Button>编辑</Button>
      <Button>删除</Button>
    </Flex>
  );
}

在用户详情页面里,可以直接写:

tsx
<UserActions />

读代码的人不需要先关心按钮怎么排列,只需要知道这里是用户操作区域。

组件封装的一个重要作用,就是让业务代码停留在合适的表达层级。

不是每一个 div 都要封装

理解到这里以后,也很容易走到另一个极端:

看到一个 div 就想新建一个组件。

例如:

tsx
<MarginTop16>
  <FlexCenter>
    <TextGray>
      内容
    </TextGray>
  </FlexCenter>
</MarginTop16>

这种封装不一定比直接写样式更好。

组件太碎以后,也会带来问题:

  • 需要记忆很多组件名称。
  • 想改一个简单样式时,要来回跳文件。
  • 组件组合层级太深。
  • 封装只是换了一个名字,没有形成稳定语义。
  • 设计规则还没稳定,接口却先固定下来了。

所以我现在觉得,不是“小组件都要封装”,而是“小组件也可以封装”。

关键要看它有没有真正发挥作用。

我会怎么判断是否值得封装

可以问自己几个问题。

1. 它是不是反复出现

如果很多地方都在写:

tsx
style={{
  display: "flex",
  alignItems: "center",
  gap: 8,
}}

那就很适合提取一个 Flex

2. 新名字能不能让代码更容易理解

比如:

tsx
<Stack />

比:

tsx
<div style={{ display: "flex", flexDirection: "column" }} />

更明确。

3. 它能不能形成统一规则

比如组件库规定间距只能从设计系统中选择:

tsx
<Flex gap="small" />
<Flex gap="medium" />
<Flex gap="large" />

这样大家不会在页面里随手写出:

tsx
gap={13}

4. 它是不是降低了业务组件的噪音

如果封装后,业务代码更容易看出页面结构,而不是被样式细节淹没,那通常值得做。

5. 它有没有隐藏掉必要能力

Flex 组件不能为了统一写法,反而让普通需求变得很难实现。

所以前面的例子保留了:

tsx
style
className
onClick
id

这些原生 div 属性。

一个基础组件应该提供常用规则,也要保留合理的扩展能力。

样式属性和语义属性怎么选

最简单的 Flex 可以直接暴露 CSS 属性:

tsx
<Flex
  align="center"
  justify="space-between"
  gap={12}
/>

这种方式直观,也比较灵活。

如果项目有稳定的设计规范,还可以把属性继续收紧:

ts
type Space = "small" | "medium" | "large";

const spaceMap: Record<Space, number> = {
  small: 4,
  medium: 8,
  large: 16,
};

然后写:

tsx
<Flex gap="medium" />

这样 gap 不再是任意数字,而是设计系统里的间距名称。

哪种更好,没有绝对答案。

如果项目刚开始,规则还没稳定,可以先保持简单。

如果多人协作、页面越来越多,或者已经有明确的设计系统,就可以逐渐收紧接口。

小组件和业务组件的区别

我现在会把组件粗略分成几类。

1. 基础布局组件

主要表达通用布局规则:

tsx
<Flex />
<Stack />
<Grid />
<Container />

2. 基础视觉组件

主要统一视觉和交互:

tsx
<Button />
<Card />
<Tag />
<Divider />

3. 复杂通用组件

通常有比较多状态和交互:

tsx
<Table />
<Form />
<Modal />
<Select />

4. 业务组件

直接表达当前系统里的业务概念:

tsx
<UserCard />
<OrderStatus />
<DeviceInfo />
<FirmwareUpgradePanel />

以前我只重视第三类,觉得复杂组件才值得封装。

后来发现,第一类和第二类虽然简单,但它们决定了业务代码是不是容易阅读。第四类则让页面结构更接近业务本身。

这些层级配合起来,维护体验才会真正变好。

children 让容器组件保持灵活

React 组件可以通过 children 接收嵌套内容:

tsx
type CardProps = {
  children: React.ReactNode;
};

function Card({ children }: CardProps) {
  return <div className="card">{children}</div>;
}

使用时:

tsx
<Card>
  <Flex justify="space-between">
    <span>标题</span>
    <Button>编辑</Button>
  </Flex>
</Card>

Card 不需要提前知道内部会放什么。

它只负责提供一个稳定的视觉容器。

React 官方文档也把这种模式用在 Card、面板、网格等视觉包装组件上。children 让简单容器组件既有明确作用,又不会绑定具体业务。

可维护性不只是减少代码行数

以前我容易把“封装”理解成减少重复代码。

这个目标当然重要,但可维护性不只是代码行数更少。

有时候封装以后,总代码量甚至会多一点:

tsx
<Flex align="center" gap={8}>
  ...
</Flex>

不一定比:

tsx
<div className="actions">
  ...
</div>

更短。

但是它减少了理解成本。

读代码的人不需要再去 CSS 文件里确认 actions 到底做了什么,也不用每次重新分析 style

代码不只是写给浏览器执行的,也是写给以后维护它的人看的。

我最后想通的地方

我刚开始写 React 时,总是在业务组件里堆很多 div

后来看到 Flex,一开始觉得它只是给:

css
display: flex;

套了一层壳,好像没有什么必要。

但是再回头看,才发现它解决的不只是样式复用问题。

它是在表达意图:

text
这里是一个横向或者纵向布局容器
这里的子元素按照某种规则排列

这种封装很简单,但效果确实不错。

我也开始明白,不是只有 TableForm 这些重组件才需要封装。

小组件也可以封装。

只要它能稳定表达一个概念,减少重复细节,让业务代码更容易理解,它就已经发挥了作用。

不过也不用为了封装而封装。

一个好的小组件,应该让代码更自然,而不是让简单的事情变复杂。