🔥 React 零基础入门(中):500 行屎山代码到组件化的蜕变

🔥 React 零基础入门(中):500 行屎山代码到组件化的蜕变

摘要 :上篇我们学了 JSX,下篇我们要学 Props 和 State。但在这之间,有一个被大多数人跳过的关键问题------为什么需要组件?什么时候拆组件?怎么拆才合理? 本篇从一个 500 行的"屎山"代码出发,带你理解组件化的本质,掌握组件拆分、组合、复用的核心思想。


📌 前言

我刚学 React 的时候,写了一个"个人主页"项目。一个 App.tsx,500 多行,里面混着导航栏、用户信息、文章列表、评论区......

css 复制代码
App.tsx(500 行)
├── 导航栏 HTML(50 行)
├── 用户信息 HTML(80 行)
├── 文章列表 HTML(200 行)
├── 评论区 HTML(120 行)
└── 底部栏 HTML(50 行)

能跑。但是:

  • 改导航栏的时候,要在 500 行里找那 50 行
  • 想在另一个页面复用导航栏?复制粘贴
  • 加一个功能?继续往这个文件里塞
  • 同事说看不懂,改不动

后来我学了"组件化",把代码拆开,世界瞬间清爽了:

xml 复制代码
App.tsx(15 行)
├── <Navbar />          → Navbar.tsx(50 行)
├── <UserInfo />        → UserInfo.tsx(80 行)
├── <ArticleList />     → ArticleList.tsx(200 行)
├── <CommentSection />  → CommentSection.tsx(120 行)
└── <Footer />          → Footer.tsx(50 行)

这就是组件化的意义。 本篇不讲语法,讲思想------为什么 React 说"一切皆为组件",以及你该怎么用好这个理念。

📖 系列文章:上篇讲了环境搭建 + JSX,下篇讲 Props + State + useEffect。本篇是中篇,补上"组件"这个被跳过的中间地带。


🎯 本文适合谁

  • 学了 JSX 但不知道什么时候该拆组件的同学
  • 写了 React 但代码全塞在一个文件里的初学者
  • 想理解"组件化思维"而不仅仅是语法的开发者
  • 面试中被问"你怎么设计组件?"答不上来的求职者

📚 一、为什么需要组件?(从问题出发)

1.1 没有组件的世界

假设你要做一个电商首页,用纯 HTML + JavaScript 写:

html 复制代码
<!-- index.html --- 1000 行 -->
<nav>
  <!-- 50 行导航栏 -->
  <ul>
    <li><a href="/">首页</a></li>
    <li><a href="/products">商品</a></li>
    <li><a href="/cart">购物车(<span id="cart-count">0</span>)</a></li>
  </ul>
</nav>

<section class="product-list">
  <!-- 200 行商品列表 -->
  <div class="product-card">
    <img src="phone.jpg" />
    <h3>手机</h3>
    <p>¥2999</p>
    <button onclick="addToCart(1)">加入购物车</button>
  </div>
  <div class="product-card">
    <img src="laptop.jpg" />
    <h3>笔记本</h3>
    <p>¥5999</p>
    <button onclick="addToCart(2)">加入购物车</button>
  </div>
  <!-- ... 重复 20 次 ... -->
</section>

问题来了:

问题 具体表现
🔴 代码重复 每个商品卡片的 HTML 结构一样,复制粘贴 20 次
🔴 难以维护 改一个卡片样式,要改 20 处
🔴 逻辑混乱 导航栏逻辑、商品逻辑、购物车逻辑混在一起
🔴 无法复用 想在另一个页面用导航栏?复制粘贴
🔴 协作困难 两个人同时改同一个文件,冲突不断

1.2 有组件的世界

用 React 的组件思维重新组织:

tsx 复制代码
// 每个组件只关心自己
function Navbar() {
  return (
    <nav>
      <ul>
        <li><a href="/">首页</a></li>
        <li><a href="/products">商品</a></li>
        <li><a href="/cart">购物车</a></li>
      </ul>
    </nav>
  );
}

function ProductCard({ name, price, image }) {
  return (
    <div className="product-card">
      <img src={image} />
      <h3>{name}</h3>
      <p>¥{price}</p>
      <button>加入购物车</button>
    </div>
  );
}

function App() {
  const products = [
    { id: 1, name: '手机', price: 2999, image: 'phone.jpg' },
    { id: 2, name: '笔记本', price: 5999, image: 'laptop.jpg' },
  ];

  return (
    <>
      <Navbar />
      <section className="product-list">
        {products.map(p => (
          <ProductCard key={p.id} {...p} />
        ))}
      </section>
    </>
  );
}

对比一下:

维度 没有组件 有组件
代码重复 复制粘贴 20 次 <ProductCard /> 用 20 次
改样式 改 20 处 改 1 处(ProductCard)
文件结构 1 个文件 1000 行 5 个文件,每个 50-100 行
复用 复制粘贴 直接 <Navbar />
协作 互相冲突 各改各的组件

💡 一句话总结:组件 = 把 UI 拆成独立的、可复用的小块。就像乐高积木------每块积木可以单独做、单独改、任意组合。

1.3 组件的本质:函数

忘记 React 的术语,回到 JavaScript 的本质:

tsx 复制代码
// 普通函数:输入数据 → 输出数据
function add(a, b) {
  return a + b;
}

// React 组件:输入数据 → 输出 UI
function UserCard({ name, age }) {
  return <h1>{name},{age}岁</h1>;
}

组件就是一个函数:输入 Props,输出 JSX。

markdown 复制代码
         Props
           ↓
    ┌─────────────┐
    │   组件函数    │  →  JSX(UI)
    └─────────────┘

这就是 React 的核心公式:UI = f(props) --- 界面是数据的函数。


📚 二、组件的 3 种创建方式

2.1 函数组件(现代主流 ✅)

tsx 复制代码
// 最简单的组件
function Hello() {
  return <h1>Hello, React!</h1>;
}

// 箭头函数写法(同样有效)
const Hello = () => {
  return <h1>Hello, React!</h1>;
};

💡 2024 年以后,只用函数组件就够了。 类组件是历史遗留,新项目不要用。

2.2 类组件(历史遗留,知道就行)

旧项目里可能会看到 class Hello extends Component { render() {} } 的写法,这是 Hooks 出现之前的主流方式。新项目不需要学它------函数组件更简洁、TypeScript 更友好、逻辑复用更方便(通过自定义 Hooks)。遇到旧代码时查文档即可。

2.3 组件命名规则

tsx 复制代码
// ✅ 正确:大写开头(PascalCase)
function UserCard() { ... }
function NavBar() { ... }
function ProductList() { ... }

// ❌ 错误:小写开头
function userCard() { ... }  // React 会认为是 HTML 标签!
function navBar() { ... }

🔑 为什么必须大写? React 通过首字母大小写来区分组件原生 HTML 标签<div> 是 HTML,<Div> 是组件。


📚 三、什么时候该拆组件?(核心问题)

这是初学者最困惑的问题。我给你一个决策流程图

复制代码
这段代码需要拆组件吗?
  │
  ├─ 它会被重复使用吗?
  │   └─ 是 → ✅ 拆!
  │
  ├─ 它超过 100 行了吗?
  │   └─ 是 → ✅ 拆!
  │
  ├─ 它有独立的功能/职责吗?
  │   └─ 是 → ✅ 拆!
  │
  ├─ 它需要独立测试吗?
  │   └─ 是 → ✅ 拆!
  │
  └─ 以上都不是 → ❌ 先不拆

3.1 信号 1:代码重复

tsx 复制代码
// ❌ 重复的代码
function App() {
  return (
    <div>
      <div className="card">
        <img src="avatar1.jpg" />
        <h3>张三</h3>
        <p>前端工程师</p>
      </div>
      <div className="card">
        <img src="avatar2.jpg" />
        <h3>李四</h3>
        <p>后端工程师</p>
      </div>
      <div className="card">
        <img src="avatar3.jpg" />
        <h3>王五</h3>
        <p>全栈工程师</p>
      </div>
    </div>
  );
}

// ✅ 提取为可复用的组件
function UserCard({ avatar, name, role }) {
  return (
    <div className="card">
      <img src={avatar} />
      <h3>{name}</h3>
      <p>{role}</p>
    </div>
  );
}

function App() {
  return (
    <div>
      <UserCard avatar="avatar1.jpg" name="张三" role="前端工程师" />
      <UserCard avatar="avatar2.jpg" name="李四" role="后端工程师" />
      <UserCard avatar="avatar3.jpg" name="王五" role="全栈工程师" />
    </div>
  );
}

3.2 信号 2:文件太长

tsx 复制代码
// ❌ 一个组件 300 行,什么都干
function Dashboard() {
  // 20 行 state
  // 30 行数据请求
  // 50 行导航栏 JSX
  // 80 行图表 JSX
  // 60 行表格 JSX
  // 40 行侧边栏 JSX
  // 20 行底部栏 JSX
  return <div>...</div>;
}

// ✅ 拆成多个小组件
function Dashboard() {
  return (
    <div>
      <Navbar />
      <Charts />
      <DataTable />
      <Sidebar />
      <Footer />
    </div>
  );
}

3.3 信号 3:独立职责

tsx 复制代码
// ❌ 一个组件干了太多事
function UserPage() {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);
  const [followers, setFollowers] = useState([]);

  // 请求用户数据
  // 请求文章数据
  // 请求粉丝数据
  // 渲染用户信息
  // 渲染文章列表
  // 渲染粉丝列表

  return (
    <div>
      {/* 用户信息 */}
      {/* 文章列表 */}
      {/* 粉丝列表 */}
    </div>
  );
}

// ✅ 每个组件只干一件事
function UserPage() {
  return (
    <div>
      <UserProfile />      {/* 只负责用户信息 */}
      <UserPosts />        {/* 只负责文章列表 */}
      <UserFollowers />    {/* 只负责粉丝列表 */}
    </div>
  );
}

3.4 不要过度拆分

tsx 复制代码
// ❌ 过度拆分:一个标签一个组件,反而更难读
function GreetingText() {
  return <span>你好</span>;
}

function UserName({ name }) {
  return <strong>{name}</strong>;
}

function Greeting() {
  return (
    <p>
      <GreetingText />,<UserName name="张三" />!
    </p>
  );
}

// ✅ 合理拆分:有意义的整体
function Greeting({ name }) {
  return <p>你好,<strong>{name}</strong>!</p>;
}

💡 原则:拆分是为了让代码更清晰、更可维护,不是为了拆而拆。如果你拆完发现文件更多了、更难找了,就说明拆过头了。


📚 四、组件组合:搭积木的艺术

React 推崇**组合(Composition)**而非继承(Inheritance)。你不需要继承一个基础组件来扩展它,而是把小组件组合成大组件。

4.1 基础组合:嵌套

tsx 复制代码
// 小组件嵌套成大组件
function Header() {
  return (
    <header>
      <Logo />
      <NavMenu />
      <SearchBar />
      <UserAvatar />
    </header>
  );
}

function App() {
  return (
    <div>
      <Header />       {/* Header 内部组合了 4 个小组件 */}
      <MainContent />
      <Footer />
    </div>
  );
}

4.2 children:万能插槽

children 是 React 组合模式的核心,它让组件变成一个"容器":

tsx 复制代码
// Card 组件:通用卡片容器
function Card({ title, children }) {
  return (
    <div className="card">
      <h2 className="card-title">{title}</h2>
      <div className="card-body">
        {children}    {/* 里面放什么都行 */}
      </div>
    </div>
  );
}

// 使用:同一个 Card,放不同的内容
function App() {
  return (
    <>
      <Card title="用户信息">
        <p>姓名:张三</p>
        <p>年龄:25</p>
      </Card>

      <Card title="文章列表">
        <ul>
          <li>文章 1</li>
          <li>文章 2</li>
        </ul>
      </Card>

      <Card title="设置">
        <button>修改密码</button>
        <button>退出登录</button>
      </Card>
    </>
  );
}

💡 children 可以是任何东西:字符串、数字、JSX、数组,甚至函数。

children 的 TypeScript 类型
tsx 复制代码
import { ReactNode } from 'react';

interface CardProps {
  title: string;
  children: ReactNode;  // ReactNode 是 children 的标准类型
}

function Card({ title, children }: CardProps) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="card-body">{children}</div>
    </div>
  );
}

🔑 ReactNode 类型可以接受string | number | JSX.Element | null | undefined | ReactNode[]。基本上"能渲染的东西"都可以。

children 的注意事项
tsx 复制代码
// ❌ children 里混入了业务逻辑(不推荐)
<Card title="用户">
  {() => {                      // children 是函数,逻辑和 UI 混在一起
    if (!user) return null;
    return <p>{user.name}</p>;
  }}
</Card>

// ✅ children 只放 UI,逻辑放在组件外部
function UserCard({ user }) {
  if (!user) return null;
  return (
    <Card title="用户">
      <p>{user.name}</p>        {/* children 是纯粹的 UI */}
    </Card>
  );
}

💡 原则:children 是"展示什么",不是"怎么获取数据"。数据逻辑放在组件内部或自定义 Hook 里。

4.3 多插槽组合

当一个组件需要多个"插槽"时,用 Props 代替 children:

tsx 复制代码
// Layout 组件:左右布局
function Layout({ sidebar, content }) {
  return (
    <div className="layout">
      <aside className="sidebar">
        {sidebar}
      </aside>
      <main className="main">
        {content}
      </main>
    </div>
  );
}

// 使用
function App() {
  return (
    <Layout
      sidebar={<NavMenu />}
      content={<ArticleList />}
    />
  );
}

多插槽的 TypeScript 写法:

tsx 复制代码
import { ReactNode } from 'react';

interface LayoutProps {
  sidebar: ReactNode;   // 接收 JSX 作为插槽
  content: ReactNode;
  header?: ReactNode;   // 可选插槽
  footer?: ReactNode;
}

function Layout({ sidebar, content, header, footer }: LayoutProps) {
  return (
    <div className="layout">
      {header && <header>{header}</header>}
      <aside>{sidebar}</aside>
      <main>{content}</main>
      {footer && <footer>{footer}</footer>}
    </div>
  );
}

4.4 包裹组件(Wrapper)

tsx 复制代码
// 通用的"带边框的容器"
function BorderedBox({ children, color = 'gray' }) {
  return (
    <div style={{
      border: `2px solid ${color}`,
      borderRadius: '8px',
      padding: '16px',
      margin: '8px 0',
    }}>
      {children}
    </div>
  );
}

// 使用:任何内容都能被包裹
function App() {
  return (
    <>
      <BorderedBox color="blue">
        <h2>通知</h2>
        <p>你有一条新消息</p>
      </BorderedBox>

      <BorderedBox color="red">
        <h2>警告</h2>
        <p>磁盘空间不足</p>
      </BorderedBox>
    </>
  );
}

4.5 组合 vs 维承

React 官方明确说:在 React 中,我们没有发现需要使用继承来构建组件层次结构的场景。

tsx 复制代码
// ❌ 继承思维(不要这样做)
class BaseButton extends Component {
  render() {
    return <button className="btn">{this.props.text}</button>;
  }
}

class PrimaryButton extends BaseButton {
  render() {
    return <button className="btn btn-primary">{this.props.text}</button>;
  }
}

// ✅ 组合思维(推荐做法)
function Button({ variant = 'default', children }) {
  return (
    <button className={`btn btn-${variant}`}>
      {children}
    </button>
  );
}

function App() {
  return (
    <>
      <Button>默认按钮</Button>
      <Button variant="primary">主要按钮</Button>
      <Button variant="danger">危险按钮</Button>
    </>
  );
}

🔑 核心理念:组合比继承更灵活。继承是"is-a"关系(PrimaryButton is-a Button),组合是"has-a"关系(App has-a Button)。React 选择组合,因为 UI 的关系更适合"包含"而非"继承"。


📚 五、组件的分类与设计模式

5.1 展示组件 vs 容器组件

⚠️ 本节涉及 useStateuseEffect,如果你还没学过 Hook,可以先跳过代码,直接看结论表格。 Hook 的详细用法在下篇讲解。

这是经典的组件设计模式:

类型 展示组件(Presentational) 容器组件(Container)
关注点 长什么样(UI) 怎么工作(逻辑)
数据来源 通过 Props 接收 自己获取数据(API、State)
是否有 State 很少或没有 通常有
是否有副作用 很少 通常有(数据请求等)
可复用性
tsx 复制代码
// 展示组件:只负责渲染 UI
function UserCard({ name, avatar, role }) {
  return (
    <div className="user-card">
      <img src={avatar} alt={name} />
      <h3>{name}</h3>
      <span>{role}</span>
    </div>
  );
}

// 容器组件:负责获取数据,传给展示组件
function UserCardContainer({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      });
  }, [userId]);

  if (loading) return <div>加载中...</div>;

  return (
    <UserCard
      name={user.name}
      avatar={user.avatar}
      role={user.role}
    />
  );
}

💡 现代 React 趋势:自定义 Hooks 取代了容器组件。把数据逻辑放进 Hook,组件只负责渲染。

tsx 复制代码
// 自定义 Hook:封装数据逻辑
function useUser(userId) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      });
  }, [userId]);

  return { user, loading };
}

// 组件:直接用 Hook
function UserCard({ userId }) {
  const { user, loading } = useUser(userId);

  if (loading) return <div>加载中...</div>;

  return (
    <div className="user-card">
      <img src={user.avatar} alt={user.name} />
      <h3>{user.name}</h3>
      <span>{user.role}</span>
    </div>
  );
}

5.2 受控组件 vs 非受控组件(表单相关)

⚠️ 本节涉及 useStateuseRef,还没学过 Hook 的同学可以先看结论表格,代码细节留到下篇。

类型 受控组件 非受控组件
数据来源 React State(value + onChange DOM 自身(ref
实时性 每次输入都更新 State 提交时才读取
适用场景 需要实时验证、联动 简单表单、性能敏感
tsx 复制代码
// 受控组件:React 控制输入框的值
function ControlledInput() {
  const [value, setValue] = useState('');

  return (
    <div>
      <input
        value={value}                    {/* State 控制值 */}
        onChange={(e) => setValue(e.target.value)}  {/* 输入更新 State */}
      />
      <p>你输入了:{value}</p>            {/* 实时显示 */}
    </div>
  );
}

// 非受控组件:DOM 自己管值
function UncontrolledInput() {
  const inputRef = useRef(null);

  const handleSubmit = () => {
    alert(inputRef.current.value);       {/* 提交时才读取 */}
  };

  return (
    <div>
      <input ref={inputRef} defaultValue="" />
      <button onClick={handleSubmit}>提交</button>
    </div>
  );
}

💡 绝大多数情况用受控组件。 非受控组件只在性能优化或集成非 React 代码时使用。

5.3 高阶组件(HOC)------ 了解即可

高阶组件是一个函数,接收组件,返回增强后的组件。在 Hooks 出现前是主流的逻辑复用方式:

tsx 复制代码
// 高阶组件:给任何组件添加"加载中"功能
function withLoading(WrappedComponent) {
  return function WithLoadingComponent({ isLoading, ...props }) {
    if (isLoading) return <div>加载中...</div>;
    return <WrappedComponent {...props} />;
  };
}

// 使用
const UserCardWithLoading = withLoading(UserCard);

function App() {
  return <UserCardWithLoading isLoading={true} name="张三" />;
}

⚠️ 现代 React 建议用自定义 Hooks 替代 HOC。 HOC 会导致组件层级嵌套过深、Props 来源不清晰。但老项目中会大量遇到,需要认识。


📚 六、组件设计 Checklist

写完一个组件后,对照这张表检查一遍:

检查项 一句话标准
单一职责 这个组件只做一件事吗?如果要描述它的功能,需要"和"字连接吗?
可复用 别的地方能用吗?还是硬编码了特定数据?
可预测 相同的 Props 输入,是否永远得到相同的 UI 输出?(不依赖外部变量)
小而专注 不超过 150 行?超过就考虑拆分
Props 接口清晰 Props 命名是否语义化?TypeScript 类型是否定义了?
命名规范 组件名 PascalCase?文件名和组件名一致?
不过度拆分 拆完之后,整体是不是更好理解了?

💡 不需要每次都完美通过所有项。 初学阶段先做到"单一职责"和"命名规范"就够了,其他项随着经验积累自然会做好。


📚 七、组件的文件组织

7.1 按功能分组(推荐)

bash 复制代码
src/
├── components/           # 通用组件
│   ├── Button/
│   │   ├── Button.tsx
│   │   └── Button.css
│   ├── Modal/
│   │   ├── Modal.tsx
│   │   └── Modal.css
│   └── index.ts          # 统一导出
├── features/             # 功能模块
│   ├── auth/
│   │   ├── LoginForm.tsx
│   │   ├── useAuth.ts    # 自定义 Hook
│   │   └── authApi.ts    # API 请求
│   ├── dashboard/
│   │   ├── Dashboard.tsx
│   │   ├── Charts.tsx
│   │   └── useDashboard.ts
│   └── profile/
│       ├── Profile.tsx
│       └── useProfile.ts
├── pages/                # 页面组件
│   ├── HomePage.tsx
│   ├── LoginPage.tsx
│   └── ProfilePage.tsx
└── App.tsx               # 根组件

7.2 单个组件的文件结构

ruby 复制代码
Button/
├── Button.tsx        # 组件代码
├── Button.test.tsx   # 测试文件
├── Button.module.css # 样式文件(CSS Modules)
└── index.ts          # 导出(方便 import)
tsx 复制代码
// Button/index.ts
export { Button } from './Button';
export type { ButtonProps } from './Button';

📚 八、实战:从"屎山"到组件化

我们把一个 300 行的"屎山"代码,一步步拆成组件化的结构。

第 1 步:识别可拆分的部分

tsx 复制代码
// ❌ 原始代码(简化版,300 行)
function App() {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);
  const [comments, setComments] = useState([]);

  // 数据请求逻辑...

  return (
    <div>
      {/* 50 行:导航栏 */}
      <nav>
        <img src={user?.avatar} />
        <span>{user?.name}</span>
        <a href="/">首页</a>
        <a href="/profile">个人中心</a>
      </nav>

      {/* 80 行:用户信息卡片 */}
      <div className="profile-card">
        <img src={user?.avatar} />
        <h2>{user?.name}</h2>
        <p>{user?.bio}</p>
        <span>粉丝:{user?.followers}</span>
        <span>关注:{user?.following}</span>
        <button>关注</button>
      </div>

      {/* 100 行:文章列表 */}
      <div className="post-list">
        {posts.map(post => (
          <div key={post.id} className="post-item">
            <h3>{post.title}</h3>
            <p>{post.summary}</p>
            <span>👍 {post.likes}</span>
            <span>💬 {post.commentCount}</span>
          </div>
        ))}
      </div>

      {/* 70 行:评论区 */}
      <div className="comments">
        <h3>评论 ({comments.length})</h3>
        {comments.map(c => (
          <div key={c.id}>
            <strong>{c.author}</strong>
            <p>{c.content}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

第 2 步:提取组件

tsx 复制代码
// Navbar.tsx --- 导航栏
function Navbar({ user }) {
  return (
    <nav className="navbar">
      {user && <img src={user.avatar} className="avatar" />}
      {user && <span>{user.name}</span>}
      <a href="/">首页</a>
      <a href="/profile">个人中心</a>
    </nav>
  );
}

// ProfileCard.tsx --- 用户信息卡片
function ProfileCard({ user, onFollow }) {
  return (
    <div className="profile-card">
      <img src={user.avatar} alt={user.name} />
      <h2>{user.name}</h2>
      <p>{user.bio}</p>
      <div className="stats">
        <span>粉丝:{user.followers}</span>
        <span>关注:{user.following}</span>
      </div>
      <button onClick={onFollow}>关注</button>
    </div>
  );
}

// PostList.tsx --- 文章列表
function PostList({ posts }) {
  return (
    <div className="post-list">
      {posts.map(post => (
        <PostItem key={post.id} post={post} />
      ))}
    </div>
  );
}

// PostItem.tsx --- 单篇文章
function PostItem({ post }) {
  return (
    <div className="post-item">
      <h3>{post.title}</h3>
      <p>{post.summary}</p>
      <div className="post-stats">
        <span>👍 {post.likes}</span>
        <span>💬 {post.commentCount}</span>
      </div>
    </div>
  );
}

// CommentSection.tsx --- 评论区
function CommentSection({ comments }) {
  return (
    <div className="comments">
      <h3>评论 ({comments.length})</h3>
      {comments.map(comment => (
        <CommentItem key={comment.id} comment={comment} />
      ))}
    </div>
  );
}

// CommentItem.tsx --- 单条评论
function CommentItem({ comment }) {
  return (
    <div className="comment-item">
      <strong>{comment.author}</strong>
      <p>{comment.content}</p>
    </div>
  );
}

第 3 步:组装

tsx 复制代码
// App.tsx --- 从 300 行变成 20 行
function App() {
  const { user, posts, comments, handleFollow } = useProfileData();

  return (
    <div className="app">
      <Navbar user={user} />
      <ProfileCard user={user} onFollow={handleFollow} />
      <PostList posts={posts} />
      <CommentSection comments={comments} />
    </div>
  );
}

改造后的组件树:

sql 复制代码
App
├── Navbar                    ← 导航栏(接收 user)
├── ProfileCard               ← 用户信息(接收 user + onFollow)
├── PostList                  ← 文章列表(接收 posts)
│   ├── PostItem              ← 单篇文章(接收 post)
│   ├── PostItem
│   └── PostItem
└── CommentSection            ← 评论区(接收 comments)
    ├── CommentItem           ← 单条评论(接收 comment)
    ├── CommentItem
    └── CommentItem

💡 组件树 = 你应用的"骨架"。 画出这棵树,你就知道数据该怎么流、组件该怎么拆。面试中被问"你怎么设计组件?"时,先画树再讲代码,面试官会很满意。

改造前后对比:

维度 改造前 改造后
文件数 1 个(300 行) 8 个(每个 20-40 行)
可读性 差(混在一起) 好(职责清晰)
可复用 不可(复制粘贴) 可(直接引用)
可测试 难(测整个页面) 易(测单个组件)
协作 冲突 各改各的

📚 九、常见踩坑清单

踩坑 原因 解决方案
所有代码塞一个文件 不知道什么时候该拆 超过 100 行或有独立职责就拆
过度拆分 为了拆而拆 拆完发现更难读就不拆
组件名小写开头 React 认为是 HTML 标签 用 PascalCase
用继承而不是组合 面向对象思维 React 官方推荐组合
Props 命名模糊 dataflagcb 用语义化命名
组件依赖外部变量 不可预测 所有数据通过 Props 传入
children 放了不该放的 混淆逻辑和展示 children 只放 UI 内容

💡 中篇总结

概念 一句话理解
为什么需要组件 拆分 UI 为独立、可复用的小块,像搭乐高积木
组件本质 输入 Props、输出 JSX 的函数
什么时候拆 代码重复 / 文件太长 / 独立职责
组合模式 children 是万能插槽,Props 是自定义插槽
设计 Checklist 单一职责、可复用、可预测、小而专注、接口清晰
展示 vs 容器 展示组件管 UI,容器组件管逻辑(现在用 Hooks 替代)
受控 vs 非受控 受控组件用 State 管值,非受控组件用 ref 读值
组合 vs 继承 React 推崇组合,不用继承

核心心法:

  1. 组件 = 函数:输入 Props,输出 UI
  2. 组合 > 继承:用 children 和 Props 组合,不用 extends
  3. 单一职责:一个组件只做一件事
  4. 可预测:相同的 Props,永远得到相同的 UI
  5. 不要过度拆分:拆是为了更清晰,不是为了拆而拆

📖 下篇预告

下一篇将讲解 React 的数据流和生命周期:

  • Props:父组件如何给子组件传数据(包括 TypeScript 写法)
  • StateuseState 的正确用法和函数式更新
  • useEffect:生命周期的终极答案(依赖数组、清理函数、竞态条件)
  • 事件处理 + 列表渲染
  • 实战:完整的待办事项 App

👉 关注我,下篇不迷路!


🔗 参考资料

官方文档:

延伸阅读:

  • Ant Design 组件库源码 --- 学习工业级组件是怎么设计的(Props 接口、组合模式、TypeScript 类型)
  • Bulletproof React --- React 最佳实践项目模板,包含组件文件组织、目录结构等

💬 交流讨论

你在拆组件的过程中踩过什么坑?或者你有什么独特的组件设计经验?欢迎在评论区分享!

觉得有用?点个赞👍收藏⭐关注👆,上篇讲了 JSX,本篇讲了组件思想,下篇继续讲 Props + State + useEffect!

相关推荐
不好听6132 小时前
React vs Vue:两大前端框架技术选型深度对比
前端·vue.js·react.js
GuWenyue2 小时前
Cursor黑盒拆解!1套LangChain.js手写Mini编程Agent,自动生成React项目,效率提升60%
前端·数据库·人工智能
GuWenyue2 小时前
传统Agent工具两大痛点!300行代码落地MCP跨语言工具,彻底解耦LLM与工具
前端·人工智能·算法
小林ixn3 小时前
从 onclick 到 React 合成事件,再到完整应用逻辑:一次前端架构的深度剖析
前端·react.js·前端框架
Csvn4 小时前
🧩「找不到模块」排查全记录——Monorepo 下 TypeScript 路径别名的 5 种「不通」与根治方案
前端
腻害兔4 小时前
【若依项目-产品经理视角】RuoYi-Vue-Pro 源码拆解:字典、短信、邮件、通知——后台系统的“基础设施四件套“!
java·前端·vue.js·产品经理·ai编程
CodexDave4 小时前
MySQL事务隔离级别与MVCC机制解析
前端·数据库·mysql·nginx·性能优化·负载均衡
Ai_easygo5 小时前
AI Agent开发入门——从ReAct到Tool Calling,拆解Agent的底层运行逻辑
前端·人工智能·react.js
研☆香5 小时前
分析制作html页面,如何划分页面结构
前端