React:组件化开发入门
这一课你将能做到
- 用函数组件和 JSX 语法拆分页面为可复用的组件
- 用 props 传递数据,用 useState 管理组件内部状态
- 用 useEffect 处理副作用(请求、定时器),用条件渲染和列表渲染构建动态界面
为什么要学
原生 JS 操作 DOM 的问题是:代码和结构混在一起,数据变了要手动找元素、改内容,页面复杂后代码变成一团乱麻。React 的核心思想是「数据驱动视图」——你只描述「数据长什么样」,React 负责高效更新 DOM。
React 是目前全球最流行的前端框架,Meta(Facebook)出品,生态最成熟,岗位需求最大。学会 React 就拿到了现代前端开发的入场券。
先建立图像
React 的组件化思想:页面不是一整块 HTML,而是一棵组件树。
App(根组件)
├── Header
│ ├── Logo
│ └── Nav
├── Main
│ ├── ArticleList
│ │ ├── ArticleItem
│ │ └── ArticleItem
│ └── Sidebar
└── Footer
每个组件是一个函数,接收输入(props),返回描述界面的 JSX。数据变化时,React 自动重新渲染受影响的组件。
跟着做
步骤 1:创建 React 项目
用 Vite 创建现代 React 项目(官方推荐):
npm create vite@latest my-react-app -- --template react
cd my-react-app
npm install
npm run dev
浏览器打开 http://localhost:5173,看到欢迎页面。项目结构:
my-react-app/
├── src/
│ ├── main.jsx ← 入口,挂载 App
│ ├── App.jsx ← 根组件
│ └── index.css
├── index.html
└── package.json
步骤 2:第一个函数组件
修改 src/App.jsx:
function App() {
return (
<div>
<h1>我的 React 应用</h1>
<p>这是第一个组件。</p>
</div>
);
}
export default App;
组件就是返回 JSX 的函数。JSX 看起来像 HTML,但其实是 JavaScript 语法糖,最终编译成 React.createElement() 调用。
步骤 3:JSX 语法规则
function App() {
const name = "学习者";
const isLoggedIn = true;
const items = ["苹果", "香蕉", "橙子"];
return (
<div className="container">
{/* 1. 插值表达式 */}
<h1>你好,{name}</h1>
{/* 2. 条件渲染:三元 */}
<p>{isLoggedIn ? "欢迎回来" : "请登录"}</p>
{/* 3. 条件渲染:与运算 */}
{isLoggedIn && <button>退出</button>}
{/* 4. 列表渲染:map + key */}
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
{/* 5. 事件处理 */}
<button onClick={() => alert("点击了")}>点我</button>
{/* 6. class 要写成 className */}
<div className="box">样式类</div>
</div>
);
}
- 必须有一个根元素(或用
<> Fragment 包裹)
- 所有标签必须闭合(
<img />、<br />)
- 属性用 camelCase:
className、onClick、htmlFor
- 插值用
{},里面可以是任何 JS 表达式
步骤 4:组件拆分与 props
把 UI 拆成独立组件,通过 props 传递数据:
// UserCard.jsx
function UserCard({ name, age, isVip }) {
return (
<div className="card">
<h3>{name}</h3>
<p>年龄:{age}</p>
{isVip && <span className="badge">VIP</span>}
</div>
);
}
export default UserCard;
// App.jsx
import UserCard from "./UserCard";
function App() {
const users = [
{ id: 1, name: "张三", age: 25, isVip: true },
{ id: 2, name: "李四", age: 30, isVip: false }
];
return (
<div>
<h1>用户列表</h1>
{users.map((user) => (
<UserCard
key={user.id}
name={user.name}
age={user.age}
isVip={user.isVip}
/>
))}
</div>
);
}
props 是只读的,子组件不能修改父组件传下来的数据。这是 React 的单向数据流原则。
步骤 5:useState 管理状态
状态是组件内部会变化的数据。用 useState Hook 声明:
import { useState } from "react";
function Counter() {
// 声明状态:count 是当前值,setCount 是更新函数
const [count, setCount] = useState(0);
return (
<div>
<p>当前计数:{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setCount(0)}>重置</button>
</div>
);
}
- 不要直接修改状态(
count++ 是错的),必须用 setCount
- 状态更新是异步的,连续调用
setCount(count + 1) 两次可能只加一次;用函数式更新 setCount(prev => prev + 1) 更可靠
- 状态变化会触发组件重新渲染
步骤 6:useEffect 处理副作用
副作用是组件渲染之外的操作:请求数据、设置定时器、操作 DOM:
import { useState, useEffect } from "react";
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
// 组件挂载后执行一次
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users")
.then((res) => res.json())
.then((data) => {
setUsers(data);
setLoading(false);
});
}, []); // 空依赖数组 = 只在挂载时执行
if (loading) return <p>加载中...</p>;
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
[]:只在挂载时执行一次
[count]:count 变化时执行
- 不写:每次渲染都执行(通常要避免)
步骤 7:表单处理
import { useState } from "react";
function LoginForm() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const handleSubmit = (e) => {
e.preventDefault(); // 阻止表单默认刷新
console.log("提交:", { username, password });
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="用户名"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="密码"
/>
<button type="submit">登录</button>
</form>
);
}
React 表单采用「受控组件」模式:输入框的值由 state 控制,输入时通过 onChange 更新 state。
讲清楚要点
- 组件是函数,JSX 是返回值:React 组件就是普通的 JavaScript 函数,接收 props,返回 JSX。没有 class 的束缚,逻辑更直白。
- 数据驱动视图:不要手动操作 DOM,而是更新 state,让 React 自动重新渲染。你写「数据长什么样」,React 管「怎么更新页面」。
- 单向数据流:数据从父组件通过 props 流向子组件,子组件通过回调函数把事件传回父组件。禁止子组件直接改 props。
- 状态是不可变的:更新对象/数组时要创建新副本,不要原地修改。
setUsers([...users, newUser]) 而不是 users.push(newUser)。
- Hook 是 React 的灵魂:
useState 管状态,useEffect 管副作用,useContext 管全局数据。函数组件 + Hook 是现代 React 的唯一写法。
你可能会懵的地方
-
为什么 class 要写成 className?
因为 JSX 最终编译成 JavaScript,class 是 JS 的保留关键字。for 同理要写成 htmlFor。
-
为什么列表要加 key?
key 帮助 React 识别哪些元素变了、新增、删除。用数组索引做 key 在列表会重排时会导致状态错乱,优先用唯一 ID。
-
为什么我的状态更新了但页面没变化?
两种可能:1) 直接修改了状态对象而不是用 set 函数;2) 修改的是同一个引用(比如 users.push()),React 的浅比较检测不到变化。
-
useEffect 里的空依赖数组到底是什么意思?
告诉 React:这个副作用不依赖任何 props 或 state,只在组件挂载时执行一次。如果不写,每次渲染都会执行,可能导致无限循环。
-
React 和 Vue/Angular 有什么区别?
React 是「库」,只负责视图层,路由、状态管理需要额外配;Vue/Angular 是「框架」,全家桶开箱即用。React 更灵活,生态最大;Vue 上手更简单。
错法对照
| 错法 | 问题 | 正解 |
|---|
count++ 直接改 state | React 检测不到变化,不重新渲染 | setCount(count + 1) |
users.push(newUser) | 原地修改数组,引用不变 | setUsers([...users, newUser]) |
JSX 里写 class= | 编译报错 | className= |
组件名小写 <userCard /> | React 当成 HTML 标签 | 组件名必须大写开头 |
在 JSX 里写 if 语句 | JSX 只能写表达式 | 用三元或逻辑与 |
useEffect 里写 async 函数 | useEffect 不支持 async 回调 | 内部定义 async 函数再调用 |
事件写 onClick="fn()" | 字符串不会执行 | onClick={fn} 或 onClick={() => fn()} |
| 列表不加 key | React 警告,状态可能错乱 | 加唯一稳定的 key |
卡点急救
- 页面白屏,控制台报错:看报错信息,通常是 JSX 语法错误(标签没闭合、属性写错)或组件名没大写。
- 状态更新了但视图没变:检查是否直接修改了 state;检查 key 是否唯一;检查是否用了异步更新后的旧值。
- useEffect 无限循环:依赖数组里放了每次渲染都新建的对象/函数,导致 effect 反复执行。用
useMemo/useCallback 缓存,或把对象移到组件外。
- 跨组件传递数据太麻烦:中间组件只是透传 props 时,考虑用
useContext 或状态管理库(Zustand/Redux)。
面试常问
-
什么是虚拟 DOM?
React 在内存中维护一个轻量的 JS 对象树(虚拟 DOM),数据变化时先对比新旧虚拟 DOM(Diff 算法),计算出最小 DOM 操作,再批量更新真实 DOM。这比直接操作 DOM 快得多。
-
函数组件和 class 组件的区别?
函数组件更简洁,配合 Hook 可以完全替代 class 组件。class 组件有生命周期方法(componentDidMount 等),函数组件用 useEffect 模拟。现代 React 只用函数组件。
-
key 的作用?
key 是 React Diff 算法的标识,帮助 React 识别哪些元素是同一个。key 稳定且唯一时,React 能高效复用 DOM 节点;key 用索引且列表会重排时,可能导致状态错乱和性能问题。
-
受控组件和非受控组件?
受控组件:表单值由 React state 控制,通过 onChange 更新;非受控组件:表单值由 DOM 自身管理,通过 ref 读取。推荐受控组件,便于校验和联动。
自测三问
- 写一个 Counter 组件,包含加一、减一、重置三个按钮。
- 为什么不能直接修改 state(如
user.name = "新名字")?正确的写法是什么?
useEffect(() => {...}, []) 和 useEffect(() => {...}) 的区别是什么?
小结一句话
React 用组件拆分界面、用 state 驱动更新、用 Hook 管理逻辑——数据一变,视图自动跟着变,你只管描述 UI 长什么样。
延伸