🔥 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 容器组件
⚠️ 本节涉及
useState和useEffect,如果你还没学过 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 非受控组件(表单相关)
⚠️ 本节涉及
useState和useRef,还没学过 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 命名模糊 | data、flag、cb |
用语义化命名 |
| 组件依赖外部变量 | 不可预测 | 所有数据通过 Props 传入 |
| children 放了不该放的 | 混淆逻辑和展示 | children 只放 UI 内容 |
💡 中篇总结
| 概念 | 一句话理解 |
|---|---|
| 为什么需要组件 | 拆分 UI 为独立、可复用的小块,像搭乐高积木 |
| 组件本质 | 输入 Props、输出 JSX 的函数 |
| 什么时候拆 | 代码重复 / 文件太长 / 独立职责 |
| 组合模式 | children 是万能插槽,Props 是自定义插槽 |
| 设计 Checklist | 单一职责、可复用、可预测、小而专注、接口清晰 |
| 展示 vs 容器 | 展示组件管 UI,容器组件管逻辑(现在用 Hooks 替代) |
| 受控 vs 非受控 | 受控组件用 State 管值,非受控组件用 ref 读值 |
| 组合 vs 继承 | React 推崇组合,不用继承 |
核心心法:
- 组件 = 函数:输入 Props,输出 UI
- 组合 > 继承:用 children 和 Props 组合,不用 extends
- 单一职责:一个组件只做一件事
- 可预测:相同的 Props,永远得到相同的 UI
- 不要过度拆分:拆是为了更清晰,不是为了拆而拆
📖 下篇预告
下一篇将讲解 React 的数据流和生命周期:
- Props:父组件如何给子组件传数据(包括 TypeScript 写法)
- State :
useState的正确用法和函数式更新 - useEffect:生命周期的终极答案(依赖数组、清理函数、竞态条件)
- 事件处理 + 列表渲染
- 实战:完整的待办事项 App
👉 关注我,下篇不迷路!
🔗 参考资料
官方文档:
- React 官方文档(中文) --- 学 React 的第一手资料
- React 官方文档 - 组件和 Props
- React 官方文档 - 组件的组合
- React 官方文档 - 保持组件纯粹
- Thinking in React --- React 官方思维模型 --- 强烈推荐,教你如何把 UI 拆成组件树
延伸阅读:
- Ant Design 组件库源码 --- 学习工业级组件是怎么设计的(Props 接口、组合模式、TypeScript 类型)
- Bulletproof React --- React 最佳实践项目模板,包含组件文件组织、目录结构等
💬 交流讨论
你在拆组件的过程中踩过什么坑?或者你有什么独特的组件设计经验?欢迎在评论区分享!
觉得有用?点个赞👍收藏⭐关注👆,上篇讲了 JSX,本篇讲了组件思想,下篇继续讲 Props + State + useEffect!