🔥 React 零基础入门(下):Props + State + useEffect 生命周期深度解析
摘要:本篇是 React 零基础入门系列的下篇,重点讲解 Props 数据传递、State 状态管理、useEffect 生命周期,以及事件处理和列表渲染。最后通过一个完整的待办事项 App 串联所有知识点。
📌 前言
上篇我们搞定了环境搭建、组件思想和 JSX 的 9 条规则。本篇继续,讲解 React 的数据流和生命周期——这是让组件"活"起来的关键。
📖 系列回顾:上篇讲了环境搭建、组件、JSX 深度解析。还没看的同学建议先看上篇。
📚 一、Props:给组件传数据(父 → 子)
1.1 基本用法
// 父组件传递数据
function App() {
return (
<div>
<UserCard
name="张三"
age={25} {/* 数字用花括号 */}
isAdmin={true} {/* 布尔值用花括号 */}
hobbies={['读书', '游泳']} {/* 数组用花括号 */}
/>
</div>
);
}
// 子组件接收数据(解构 props)
function UserCard({ name, age, isAdmin, hobbies }) {
return (
<div>
<h2>{name}</h2>
<p>年龄:{age}</p>
<p>{isAdmin ? '管理员' : '普通用户'}</p>
<p>爱好:{hobbies.join('、')}</p>
</div>
);
}
1.2 TypeScript 写法
interface UserCardProps {
name: string;
age: number;
isAdmin?: boolean; // ? 表示可选
hobbies?: string[];
}
function UserCard({ name, age, isAdmin = false, hobbies = [] }: UserCardProps) {
return (
<div>
<h2>{name}</h2>
<p>年龄:{age}</p>
<p>{isAdmin ? '管理员' : '普通用户'}</p>
<p>爱好:{hobbies.join('、')}</p>
</div>
);
}
1.3 children:组件的"插槽"
当组件标签之间有内容时,这些内容会作为 children 传入:
function Card({ title, children }) {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-body">
{children} {/* 渲染标签之间的内容 */}
</div>
</div>
);
}
// 使用
<Card title="我的卡片">
<p>这段文字会作为 children 传入</p>
<button>点击我</button>
</Card>
💡 children 是什么类型? 可以是 JSX、字符串、数字、数组,甚至是一个函数。
1.4 Props 核心规则
| 规则 | 说明 |
|---|---|
| 只读 | 子组件不能修改 Props |
| 单向流 | 数据只能从父组件流向子组件 |
| 任意类型 | 可以传字符串、数字、布尔、数组、对象、函数 |
| 默认值 | 可以用解构设置默认值 |
| children | 标签之间的内容会作为 children 传入 |
💡 Props Drilling 问题:当数据需要从顶层传到深层子组件时,中间层被迫传递不需要的 Props。解决方案是
Context或状态管理库(进阶内容)。
📚 二、State:让组件"记住"数据
2.1 useState 基本用法
import { useState } from 'react';
function Counter() {
// 格式:const [状态变量, 更新函数] = useState(初始值)
const [count, setCount] = useState(0);
return (
<div>
<p>你点击了 {count} 次</p>
<button onClick={() => setCount(count + 1)}>点我</button>
</div>
);
}
2.2 不同数据类型的 State
function Demo() {
const [count, setCount] = useState(0); // 数字
const [name, setName] = useState(''); // 字符串
const [user, setUser] = useState(null); // 对象或 null
const [todos, setTodos] = useState([]); // 数组
const [loading, setLoading] = useState(false); // 布尔值
const handleClick = () => {
setCount(count + 1); // 数字:直接加
setName('李四'); // 字符串:直接赋值
setUser({ id: 1, name: '张三' }); // 对象:替换整个对象
setTodos([...todos, '新任务']); // 数组:创建新数组
};
return <button onClick={handleClick}>更新状态</button>;
}
2.3 函数式更新:基于上一个状态计算
function Counter() {
const [count, setCount] = useState(0);
// ❌ 有问题的写法:连续调用时会出 Bug
const handleClick = () => {
setCount(count + 1); // 0 → 1
setCount(count + 1); // 0 → 1(还是旧值!)
setCount(count + 1); // 0 → 1(还是旧值!)
// 结果:count = 1,不是 3
};
// ✅ 正确写法:用函数式更新,接收上一个状态值
const handleClickCorrect = () => {
setCount(prev => prev + 1); // 0 → 1
setCount(prev => prev + 1); // 1 → 2
setCount(prev => prev + 1); // 2 → 3
// 结果:count = 3 ✅
};
return <button onClick={handleClickCorrect}>count: {count}</button>;
}
🔑 什么时候用函数式更新? 当新状态依赖旧状态时(如计数器、队列操作),用
prev => prev + 1更安全。
2.4 State 核心规则
| 规则 | 说明 |
|---|---|
| 必须用 setter | 永远不要直接修改:count++ ❌,setCount(count + 1) ✅ |
| 触发重渲染 | 状态改变 → 组件自动重新渲染 → UI 更新 |
| 批量更新 | React 会批量处理多个 setState,异步合并 |
| 不可变 | 更新对象/数组时,要创建新引用 |
| 函数式更新 | 新状态依赖旧状态时,用 setX(prev => newVal) 更安全 |
💡 核心心法:
UI = f(state)— 界面是状态的函数。你只需要管理状态,React 自动更新界面。
📚 三、生命周期与 useEffect:让组件"活"起来(重点)
3.1 什么是副作用?
组件不仅仅是在页面上渲染 UI,它还需要做一些"额外的事情":
- 从服务器获取数据
- 设置定时器
- 手动修改 DOM
- 订阅 WebSocket
- 写入 localStorage
这些"额外的事情"就叫副作用(Side Effect)。React 提供了 useEffect Hook 来处理它们。
3.2 useEffect 基本语法
import { useState, useEffect } from 'react';
function App() {
const [count, setCount] = useState(0);
// useEffect(回调函数, 依赖数组)
useEffect(() => {
document.title = `你点击了 ${count} 次`;
}, [count]); // 依赖数组:count 变化时才执行
return <button onClick={() => setCount(count + 1)}>点我</button>;
}
语法拆解:
useEffect(() => {
// 副作用逻辑
return () => {
// 清理函数(可选)
};
}, [依赖1, 依赖2]);
3.3 依赖数组的 3 种形态
// 形态 1:不传依赖数组 → 每次渲染都执行
useEffect(() => {
console.log('每次渲染都会执行');
});
// 形态 2:空数组 → 只在组件挂载时执行一次
useEffect(() => {
console.log('只在组件首次渲染时执行');
}, []);
// 形态 3:指定依赖 → 依赖变化时执行
useEffect(() => {
console.log('count 变化时执行');
}, [count]);
| 依赖数组 | 执行时机 | 类比生命周期 |
|---|---|---|
| 不传 | 每次渲染后 | componentDidMount + componentDidUpdate |
[] | 首次渲染后 | componentDidMount |
[a, b] | a 或 b 变化时 | componentDidUpdate(指定条件) |
3.4 清理函数:组件"临终遗言"
有些副作用需要在组件卸载前清理,比如定时器、事件监听器:
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// 返回清理函数(组件卸载时执行)
return () => {
clearInterval(timer);
console.log('定时器已清理');
};
}, []);
return <p>已经过了 {seconds} 秒</p>;
}
🔑 执行顺序:组件渲染 → 执行副作用 → 组件卸载前 → 执行清理函数
3.5 实战:数据请求
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUsers = async () => {
try {
setLoading(true);
const response = await fetch('https://api.example.com/users');
const data = await response.json();
setUsers(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchUsers();
}, []);
if (loading) return <p>加载中...</p>;
if (error) return <p>出错了:{error}</p>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
⚠️ 注意:
useEffect的回调函数不能直接是async函数,需要在内部定义一个async函数再调用。
3.6 实战:监听窗口大小
function WindowSize() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
// 清理:移除监听器
return () => window.removeEventListener('resize', handleResize);
}, []);
return <p>窗口宽度:{width}px</p>;
}
3.7 实战:竞态条件与 AbortController
当依赖变化触发新的数据请求时,旧请求可能比新请求晚返回,导致页面显示过时数据:
// ❌ 有竞态问题的写法
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data)); // 旧请求可能覆盖新数据
}, [userId]);
return <div>{user?.name}</div>;
}
解决方案:用 AbortController 取消旧请求
// ✅ 正确写法
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${userId}`, {
signal: controller.signal
})
.then(res => res.json())
.then(data => setUser(data))
.catch(err => {
if (err.name !== 'AbortError') {
console.error(err);
}
});
// 清理函数:取消过期请求
return () => controller.abort();
}, [userId]);
return <div>{user?.name}</div>;
}
💡 执行顺序:userId 变化 → abort 旧请求 → 发新请求
3.8 useEffect 常见踩坑
| 踩坑 | 原因 | 解决方案 |
|---|---|---|
| 无限循环 | useEffect 里更新状态,触发重渲染 | 检查依赖数组,确保有终止条件 |
| 忘记依赖 | 用了外部变量但没加到依赖数组 | ESLint 插件会警告,补上依赖 |
| 直接写 async | useEffect 回调不能是 async | 在内部定义 async 函数再调用 |
| 竞态条件 | 旧请求覆盖新请求 | 用 AbortController 取消旧请求 |
3.9 生命周期速记图
组件首次渲染
↓
执行 useEffect(依赖=[]) ← componentDidMount
↓
状态更新 → 组件重新渲染
↓
执行清理函数(上次的 effect) ← componentWillUnmount(旧)
执行 useEffect(依赖变化) ← componentDidUpdate
↓
组件卸载
↓
执行清理函数 ← componentWillUnmount
📚 四、事件处理:响应用户操作
function Form() {
const [inputValue, setInputValue] = useState('');
const [items, setItems] = useState([]);
const handleChange = (event) => {
setInputValue(event.target.value);
};
const handleSubmit = (event) => {
event.preventDefault(); // 阻止页面刷新!
if (inputValue.trim()) {
setItems([...items, inputValue]);
setInputValue('');
}
};
const handleDelete = (index) => {
setItems(items.filter((_, i) => i !== index));
};
return (
<div>
<form onSubmit={handleSubmit}>
<input
type="text"
value={inputValue}
onChange={handleChange}
placeholder="输入内容..."
/>
<button type="submit">添加</button>
</form>
<ul>
{items.map((item, index) => (
<li key={index}>
{item}
<button onClick={() => handleDelete(index)}>删除</button>
</li>
))}
</ul>
</div>
);
}
要点: 事件名用驼峰、接收 event 对象、表单要 preventDefault、传参用箭头函数包裹。
📚 五、列表渲染:用 map 循环
function TodoList() {
const todos = [
{ id: 1, text: '学习 React', done: true },
{ id: 2, text: '做个项目', done: false },
{ id: 3, text: '写篇文章', done: false },
];
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<span style={{
textDecoration: todo.done ? 'line-through' : 'none'
}}>
{todo.text}
</span>
</li>
))}
</ul>
);
}
关键点: 用 map 遍历数组,每个元素必须有唯一的 key(用 ID,不要用 index)。
📝 实战:一个完整的待办事项 App
把以上所有概念组合起来:
import { useState } from 'react';
function App() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');
// 添加待办
const addTodo = () => {
if (input.trim()) {
setTodos([...todos, { id: Date.now(), text: input, done: false }]);
setInput('');
}
};
// 切换完成状态
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
));
};
// 删除待办
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div style={{ padding: '20px', maxWidth: '400px', margin: '0 auto' }}>
<h1>📝 待办清单</h1>
<div style={{ display: 'flex', gap: '10px' }}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && addTodo()}
placeholder="输入待办..."
style={{ flex: 1, padding: '8px' }}
/>
<button onClick={addTodo}>添加</button>
</div>
<ul style={{ listStyle: 'none', padding: 0 }}>
{todos.map(todo => (
<li key={todo.id} style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '8px 0',
borderBottom: '1px solid #eee'
}}>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id)}
/>
<span style={{
flex: 1,
textDecoration: todo.done ? 'line-through' : 'none'
}}>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>删除</button>
</li>
))}
</ul>
{todos.length === 0 && <p style={{ color: '#999' }}>还没有待办,添加一个吧!</p>}
<p>总计:{todos.length} 项</p>
</div>
);
}
export default App;
这个 App 用到了哪些概念?
| 概念 | 用法 |
|---|---|
| 组件 | function App() |
| JSX | {input}、{todos.map(...)} |
| State | useState([])、useState('') |
| 事件 | onClick、onChange、onKeyDown |
| 列表渲染 | todos.map(todo => <li key={todo.id}>) |
| 条件渲染 | {todos.length === 0 && <p>...</p>} |
💡 下篇总结
| 概念 | 一句话理解 |
|---|---|
| Props | 父传子的数据,只读,单向流 |
| State | 组件内部可变数据,用 setState 更新,触发重渲染 |
| useEffect | 处理副作用,依赖数组控制执行时机,返回清理函数 |
| 事件 | 驼峰命名,传函数引用,表单要 preventDefault |
| 列表 | 用 map 渲染,每个元素必须有唯一的 key |
核心心法:
UI = f(state)— 界面是状态的函数- 数据单向流:父 → 子(Props),子 → 父(回调函数)
- 状态不可变:永远用
setState,不要直接修改 - 副作用用
useEffect,清理函数在卸载前执行 - 组件化思维:拆分 UI 为独立、可复用的小块
🗺️ 下一步学什么?
| 阶段 | 学习内容 | 推荐资源 |
|---|---|---|
| 进阶 1 | React Router — 多页面路由 | react-router-dom |
| 进阶 2 | 自定义 Hooks — 封装复用逻辑 | React 官方文档 |
| 进阶 3 | Context — 跨组件传值 | React 官方文档 |
| 进阶 4 | 性能优化 — useMemo、useCallback、React.memo | React 官方文档 |
| 进阶 5 | 状态管理 — Zustand 或 Redux | Zustand 官方文档 |
| 进阶 6 | React 19 新特性 — Server Components、use() | React 官方博客 |
🔗 参考资料
💬 交流讨论
你在学 React 的过程中踩过什么坑?欢迎在评论区分享!
觉得有用?点个赞👍收藏⭐关注👆,后续继续更新 React Router 实战、React 19 新特性!