主题
组件通信与 Hooks(二)
本页对应 组件通信与 Hooks,共 7 段视频,约 1 小时 32 分。
学习目标
- 能够说明 useEffect 的作用与副作用概念,并使用依赖项数组分别实现组件挂载、更新、卸载时的逻辑及清理函数。
- 能够按“一个 useEffect 负责一个完整功能”组织代码,并解释依赖项指定原则及遗漏依赖可能导致的问题。
- 能够使用 useEffect 在组件初次渲染时发送请求获取数据,正确创建异步函数并避免直接在 effect 回调上使用 async。
- 能够说明 React Hooks 的含义与调用规则,识别并修正条件或循环中调用 Hook 的错误。
- 能够使用父子组件通讯与状态提升实现知乎频道弹窗的展示与隐藏。
- 能够使用 Context 跨组件共享频道数据,并通过更新频道选中状态完成频道渲染、移除与添加。
知识点
一、useEffect 要解决什么问题
为什么需要:React 组件也有挂载、更新、卸载三个阶段。像发网络请求、操作浏览器 DOM 这类“副作用”代码,不能随便写在渲染逻辑里,否则容易重复执行或时机不对。转写稿里明确说,useEffect 类似于生命周期钩子函数的作用,可以在三个阶段执行副作用代码;它名字里的 effect 就来自 side effects(副作用)。
怎么想:先判断这段代码属于“组件一出现就做”“某个数据变了再做”,还是“组件卸载前清理”。不同目标对应不同依赖项写法。
怎么做:调用 useEffect,第一个参数是 effect 函数,第二个参数是依赖项数组。挂载时只执行一次,就传空数组:
jsx
useEffect(() => {
console.log('建立连接 music 房间')
}, [])调试时刷新页面,控制台会打印“建立连接 music 房间”;再切换房间,这段代码不再执行,说明空数组确实只在挂载时走一次。
易错点:不要把“空数组”理解成“什么都不依赖所以永远不执行”,它表示只在挂载时执行一次。转写稿也提醒:useEffect 只是“类似于”生命周期钩子,并不完全等同。
二、用依赖项模拟挂载、更新、卸载
为什么需要:聊天室案例要分别实现挂载时连接、更新时连最新房间、卸载时断开连接。只靠一个空数组不够。
怎么想:更新阶段的关键是“哪个数据变了要重新执行”。案例里是 roomId,所以把它放进依赖项数组。卸载阶段要在 effect 函数里 return 一个清理函数。
怎么做:
jsx
// 更新时:roomId 变化就重新执行
useEffect(() => {
console.log('建立连接', roomId, '房间')
}, [roomId])
// 卸载时:返回清理函数
useEffect(() => {
return () => {
console.log('断开连接')
}
}, [])转写稿的调试过程是:依赖项写 [roomId] 后,一进页面也会执行一次,切换 travel 会再次执行;这正说明它“挂载和更新都会执行”。卸载演示则要点击“退出聊天”,让聊天室组件不再渲染,此时清理函数才打印“断开连接”。
易错点:卸载时依赖项仍然写空数组,因为挂载和卸载都只发生一次;清理代码必须写在 return 的函数里,不能直接写在 effect 外层。
三、一个 useEffect 负责一个完整功能
为什么需要:如果按生命周期拆分,建立连接写在挂载 effect,更新连接写在更新 effect,断开连接写在卸载 effect,维护时要上下滚动找代码。转写稿明确推荐“一个 useEffect 负责一个完整功能”。
怎么想:把“建立连接、更新连接、清理旧连接”都放到同一个 effect 中,依赖项仍然用会变化的 roomId。
怎么做:
jsx
useEffect(() => {
console.log('建立连接', roomId, '房间')
return () => {
console.log('清理连接', roomId)
}
}, [roomId])调试时能看到:挂载只建立连接;切换房间时先清理上一个房间,再建立新房间;卸载时只走清理函数。转写稿特别解释:这里的 return 不再只代表卸载,更准确地说是“清理函数”,在做下一个动作前先清理上一次。
易错点:看到更新时打印“断开连接”不要误以为组件卸载了,它可能只是清理上一次连接。
四、依赖项怎么定:用到且可变
为什么需要:实际开发中依赖项要自己判断。转写稿给出原则:effect 函数中用到了,并且这个值是可变的,就应该作为依赖项。常见可变值包括 props、state、组件中创建的变量;组件外创建或导入的变量、函数对组件来说不变,不需要作为依赖项。
怎么做:如果 effect 里用了 count 却没写进依赖项,React 会给警告,提示丢失 dependency。按提示补上即可:
jsx
useEffect(() => {
console.log(count)
}, [count])易错点:不要为了消警告乱写依赖项。转写稿演示了极端情况:所有依赖都不写,代码就只在挂载时执行一次、卸载时清理一次,更新时不会按预期执行,容易出现 bug。
五、用 useEffect 发请求的正确写法
为什么需要:组件初次渲染时发请求获取数据是常见场景。转写稿步骤是:调用 useEffect,依赖项传空数组;在 effect 内创建一个新函数;在新函数上加 async;调用这个函数;函数里发请求。
jsx
useEffect(() => {
const loadData = async () => {
const res = await axios.get('接口地址')
setTodos(res.data)
}
loadData()
}, [])易错点:不要直接把 effect 函数写成 async:
jsx
useEffect(async () => {
// 会警告
}, [])转写稿解释:effect 回调应当是同步的,加 async 会变成异步,React 会警告,正确做法是内部再创建异步函数。另一个易错点是:不是所有请求都放 useEffect。只有跟挂载、更新、卸载相关的请求才放;用户点击事件里的请求直接写在事件处理程序中。
六、Hooks 的调用规则与案例落地
为什么需要:转写稿说明 Hooks 是以 use 开头的函数,用来给组件提供不同 React 特性,如 useState、useEffect、useContext。它们依赖调用顺序,所以只能在组件顶层调用,不能嵌套在 if、for 或其他函数中。
怎么做:正确写法是直接在函数组件中调用;错误写法如下,会报“conditionally 调用 Hooks”的警告:
jsx
if (Math.random() > 0.5) {
useState()
}在知乎频道管理案例中,转写稿用 useEffect 在 App 组件中请求频道数据,再用 context 共享给 Home 和频道弹窗;还专门强调要把 context 对象单独放在一个 JS 文件中导出,保证提供数据和使用数据的是同一个 context 对象。
易错点:Hooks 不要条件调用;依赖项要按提示正确指定;useEffect 的清理函数在更新时也可能执行,不要只把它当卸载钩子。
从“按生命周期拆功能”到“按功能聚合”
学习 useEffect 的扩展,先要理解一个开发习惯的转变:上一节我们是用 useEffect 分别模拟挂载、更新、卸载三个阶段,把“建立连接、更新连接、断开连接”拆到不同位置。这样写能跑通,但维护时要上下滚动代码:先看挂载时做了什么,再看更新时做了什么,最后看卸载时做了什么。功能一复杂,很容易漏看。
转写稿里明确推荐:一个 useEffect 负责一个完整功能。为什么需要这样?因为实际开发是按功能推进的,一个功能的代码应尽量放在一起。怎么想?把“在线聊天室”看成一个完整功能:挂载时建立连接、房间变化时先清理旧连接再建立新连接、卸载时断开连接,这些都属于同一功能。怎么做?只写一个 useEffect,把依赖项设为会变化的 roomId,并在其中返回清理函数。示意如下:
jsx
useEffect(() => {
// 1. 建立连接
console.log('建立连接', roomId)
return () => {
// 2. 清理上一次连接
console.log('断开连接', roomId)
}
}, [roomId])这样写后,调试时要观察执行顺序。转写稿中给代码加序号验证:挂载时只走“建立连接”;切换房间时先走“断开连接”,再走“建立连接”;退出聊天、组件卸载时只走“断开连接”。这解释了一个易错点:return 返回的函数不只是“卸载时执行”,在有依赖项时,它更准确的身份是清理函数——每次依赖变化、执行下一次 effect 之前,先清理上一次。切换房间时先断开 music 再连接 travel,是合理的,不是组件卸载了。
依赖项到底该写什么
第二个扩展是依赖项说明。为什么需要专门讲?因为实际开发中 useEffect 是你自己调用的,依赖项不能靠背案例,而要靠判断。转写稿给出的原则是:effect 函数中用到的、并且可能变化的值,才应该作为依赖项。常见可变值包括 props、state、组件中创建的变量等;组件外创建或导入的变量、函数,对组件来说通常不变,不需要作为依赖项。
怎么想?先看 effect 里用没用到某个值,再看这个值会不会变。比如 roomId 在 effect 中用到了,又来自 props,父组件可能传不同房间,所以它可变,应放进依赖项。怎么做?把 roomId 放进数组:[roomId]。如果只在挂载时执行一次,才用空数组 []。
调试时 React 会给出提示。转写稿中在 effect 里打印 count 或组件内变量 age,但没有写进依赖项,控制台会警告“缺失依赖项”。这说明 React 能识别 effect 中用到的可变值。易错点有两个:一是看到警告不处理,代码可能不按预期执行;二是极端地把依赖项写成空数组,导致本该随数据变化重新执行的逻辑只执行一次。转写稿演示了不正确指定依赖时,切换房间不再执行,退出时才执行清理,行为就和预期不一致。因此要记住:一定要正确指定 useEffect 依赖项,否则容易出现 bug。
最后注意,useEffect 可以调用多次,一个负责一个完整功能,多个功能就写多个 useEffect。这比把功能拆散到不同生命周期位置更合理,也是 useEffect 与普通生命周期钩子函数的重要区别。
先明确:本节要解决什么问题
为什么需要:实际开发中经常遇到“组件初次渲染时就要发请求拿数据并使用”的场景。比如待办列表一进页面就从接口取任务数据。这个请求不能随便写在组件函数体里,否则每次渲染都可能重复发送。
怎么想:React 组件挂载、更新、卸载对应生命周期。useEffect 可以处理这些阶段的副作用,网络请求就属于副作用。只在初次渲染执行,就把依赖项写成空数组。
怎么做:调用 useEffect,第一个参数传函数,第二个参数传 [],在函数中发请求。
易错点:不要以为 useEffect 和 Vue 生命周期钩子完全一样;它只是类似。空数组表示只执行一次,不是“什么都不执行”。
正确写法:空依赖 + 内部 async 函数 + 主动调用
为什么需要:请求通常是异步的,需要 async/await;但 useEffect 的 effect 回调本身要求是同步函数,不能直接把回调写成 async。
怎么想:外层 useEffect 保持同步,在内部创建一个 async 函数,再立即调用它。这样既满足同步要求,又能使用 await。
怎么做:核心代码如下(接口地址按实际替换):
jsx
useEffect(() => {
const loadData = async () => {
const res = await axios.get('接口地址')
setTodo(res.data)
}
loadData()
}, [])调试过程:保存后刷新页面,控制台能看到请求发出;把结果 res.data 存入状态后,列表能渲染。切换页面数据时,因为依赖项是空数组,这段请求不会反复执行,说明它只在挂载阶段执行了一次。
易错点:不要写成下面这样:
jsx
useEffect(async () => {
const res = await axios.get('接口地址')
setTodo(res.data)
}, [])这样写会收到警告:effect 回调应该是同步的,而 async 会让它变成异步函数。正确做法是把 async 放到内部新建的函数上。
请求结果要存到状态里,再驱动渲染
为什么需要:请求拿到的数据如果只打印,页面不会更新;React 需要状态变化来触发重新渲染。
怎么想:接口数据是“外部数据”,组件内用 useState 保存一份,请求成功后调用更新函数。
怎么做:先准备状态,例如 const [todo, setTodo] = useState([]);请求成功后 setTodo(res.data);模板中再对 todo 做列表渲染。
易错点:不要直接修改原状态;也不要把请求结果只放在普通变量里,否则状态不变,界面不会按预期更新。
不是所有请求都放进 useEffect
为什么需要:useEffect 主要处理跟挂载、更新、卸载相关的副作用。用户点击按钮后发请求,属于事件处理逻辑,直接写在事件处理函数中更直接。
怎么想:判断请求的触发源。如果是“一进页面就拿数据”,放 useEffect;如果是“点击切换完成状态”“点击删除”等用户操作,放事件处理函数。
怎么做:例如点击切换任务完成状态时,直接发 patch 请求:
jsx
const handleToggle = async (id, done) => {
await axios.patch(`接口地址/${id}`, { done })
}调试过程:点击后接口数据被修改,刷新页面后修改仍然保留,说明请求确实发给了服务器,而不是只改了本地状态。若只改本地状态,刷新后会恢复默认数据。
易错点:不要把所有请求都塞进 useEffect。跟用户点击相关的请求,放在事件处理程序中即可;只有跟组件生命周期阶段相关的请求,才优先考虑 useEffect。
小结与检查清单
- 初次渲染发请求:
useEffect(() => { ... }, [])。 - 异步请求:内部创建
async函数并调用,不要直接useEffect(async () => {})。 - 拿到数据后:存入
useState,再渲染。 - 用户点击触发的请求:写在事件处理函数里。
- 检查控制台警告:依赖项缺失、async effect 警告都要处理。
代码示例
示例 1:useEffect 模拟挂载 / 更新 / 卸载(在线聊天室)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>useEffect 模拟生命周期 - 在线聊天室</title>
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
const rooms = [
{ id: 'music', name: '音乐' },
{ id: 'travel', name: '旅游' },
{ id: 'sports', name: '体育' },
];
// 子组件:只负责“聊天室连接”这一个完整功能
function ChatRoom({ roomId }) {
useEffect(() => {
// ① 挂载时、以及 roomId 变化后都会执行
console.log('建立连接:', roomId);
// ② 返回的清理函数:卸载时执行,依赖变化前也会先执行
return () => {
console.log('断开连接:', roomId);
};
}, [roomId]); // ③ 依赖项:roomId 一变就重新执行
return <h3>当前房间:{roomId}</h3>;
}
function App() {
const [roomId, setRoomId] = useState('music');
const [chatting, setChatting] = useState(true); // 控制子组件挂载 / 卸载
return (
<div>
<select value={roomId} onChange={(e) => setRoomId(e.target.value)}>
{rooms.map((r) => (
<option key={r.id} value={r.id}>{r.name}</option>
))}
</select>
<button onClick={() => setChatting((c) => !c)}>
{chatting ? '退出聊天' : '进入聊天'}
</button>
{chatting && <ChatRoom roomId={roomId} />}
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>要点:
useEffect(() => {...}, [])传空数组 → 只在挂载时执行一次,相当于挂载钩子。- 依赖项里放会变化的数据(这里是
roomId)→ 该数据一变就重新执行,相当于更新钩子。 - 卸载逻辑写在 return 的函数里,依赖项仍是空数组(挂载和卸载都只发生一次)。
- 易错点:
return的函数不只用于“卸载”,它是清理函数——依赖变化时会先执行清理,再执行新的 effect,所以切换房间时打印顺序是「断开旧房间 → 建立新房间」。 - 推荐写法:一个 useEffect 负责一个完整功能(连接、更新连接、断开连接全放在一处),而不是按挂载/更新/卸载拆到三个 useEffect 里。
示例 2:useEffect 发送网络请求
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>useEffect 发送请求</title>
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<!--
接口准备:
1) 新建 data.json:{ "todos": [ { "id": 1, "name": "吃饭", "done": false } ] }
2) 执行:npx json-server --watch data.json --port 8080
-->
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
const BASE_URL = 'http://localhost:8080';
function TodoItem({ item, onToggle }) {
return (
<li>
<input
type="checkbox"
checked={item.done}
onChange={() => onToggle(item.id, !item.done)}
/>
{item.name}
</li>
);
}
function App() {
const [todos, setTodos] = useState([]);
useEffect(() => {
// ① 自己创建新函数,async 加在新函数上
const loadData = async () => {
const res = await axios.get(`${BASE_URL}/todos`);
setTodos(res.data); // ② 拿到数据后存进 state
};
loadData(); // ③ 别忘了调用
}, []); // ④ 空数组:只在组件初次渲染时执行一次
// ⑤ 用户点击触发的请求,直接写在事件处理函数里,不放 useEffect
const handleToggle = async (id, done) => {
await axios.patch(`${BASE_URL}/todos/${id}`, { done });
setTodos((prev) => prev.map((t) => (t.id === id ? { ...t, done } : t)));
};
return (
<ul>
{todos.map((t) => (
<TodoItem key={t.id} item={t} onToggle={handleToggle} />
))}
</ul>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>要点:
- 依赖项写空数组,保证请求只在初次渲染时发一次。
- 易错点:不能直接给 effect 回调加
async,因为 effect 回调必须是同步的,加了async会返回 Promise 而不是清理函数,React 会给出警告。正确做法是内部新建loadData函数、在它上面加async,然后调用。 - 易错点:并不是所有请求都写在 useEffect 里。跟组件挂载 / 更新 / 卸载相关的请求才放 useEffect;用户点击触发的请求直接写在事件处理函数里,更直接。
- 请求回来后要
setTodos(res.data)存进状态,页面才能列表渲染出来;修改数据(PATCH)后同步更新本地状态,否则刷新会回到旧数据。
示例 3:Context 共享频道数据 + 添加 / 移除频道
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>频道管理 - Context 共享数据</title>
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<!--
接口准备(data.json):
{ "channels": [
{ "id": 1, "name": "全站", "selected": true, "canEdit": false },
{ "id": 2, "name": "直播", "selected": true, "canEdit": true },
{ "id": 3, "name": "健康", "selected": false, "canEdit": true }
] }
执行:npx json-server --watch data.json --port 8080
-->
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect, useContext, createContext } = React;
// ① 真实项目中应单独建 channels.js 并导出,
// 保证「提供数据的组件」和「使用数据的组件」用的是同一个 context 对象
const ChannelContext = createContext(null);
function App() {
const [channels, setChannels] = useState([]);
// ② 挂载时发请求拿频道数据
useEffect(() => {
const loadData = async () => {
const res = await axios.get('http://localhost:8080/channels');
setChannels(res.data);
};
loadData();
}, []);
// ③ 谁提供状态谁提供修改状态的函数
const updateChannel = (id, selected) => {
setChannels((prev) =>
prev.map((c) => (c.id === id ? { ...c, selected } : c))
);
};
// ④ 按是否选中拆成两组数据
const myChannels = channels.filter((c) => c.selected);
const moreChannels = channels.filter((c) => !c.selected);
return (
<ChannelContext.Provider value={{ myChannels, moreChannels, updateChannel }}>
<Home />
<ChannelModal />
</ChannelContext.Provider>
);
}
function Home() {
const { myChannels } = useContext(ChannelContext);
return (
<div>
<h4>首页频道</h4>
{myChannels.map((c) => <span key={c.id}>{c.name} </span>)}
</div>
);
}
function ChannelModal() {
const { myChannels, moreChannels, updateChannel } = useContext(ChannelContext);
return (
<div>
<h4>我的频道</h4>
{myChannels.map((c) => (
<span key={c.id} onClick={() => c.canEdit && updateChannel(c.id, false)}>
{c.name}{c.canEdit ? ' ×' : ''}{' '}
</span>
))}
<h4>更多频道</h4>
{moreChannels.length > 0 ? (
moreChannels.map((c) => (
<span key={c.id} onClick={() => updateChannel(c.id, true)}>
{c.name}{' '}
</span>
))
) : (
<div>已全部添加至我的频道</div>
)}
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>要点:
- 易错点:
createContext()必须在组件外部执行一次;如果每个组件各自createContext,就不是同一个对象,useContext拿不到数据。所以真实项目要单独建文件导出 context。 Provider必须提供value属性,否则报错;共享多个值就传一个对象{ myChannels, moreChannels, updateChannel }。- 添加 / 移除频道本质只是改
selected这个状态:true就在「我的频道」,false就在「更多频道」,不需要写两套逻辑。 - 易错点:状态在 App 中,修改状态的函数也必须由 App 提供并一起共享出去(谁的状态谁负责),子组件只负责调用。
- 不可编辑频道用
canEdit控制:canEdit为false时不渲染叉号,点击也不触发移除。 - 「更多频道」为空时做条件渲染,展示「已全部添加至我的频道」。
常见错误
现象:切换房间后仍打印旧连接,或请求只在首次渲染执行。
原因:依赖数组为空,或漏写roomId等可变值。
修法:把 effect 中用到的 props、state、组件内可变变量补进依赖数组,按 lint 提示修正。现象:
useEffect回调直接写成async后出现警告。
原因:effect 回调应保持同步,async会返回 Promise,破坏清理函数约定。
修法:在 effect 内部定义async函数并立即调用,外层回调不加async。现象:依赖变化或组件卸载时,旧请求/旧连接没有被清理。
原因:只写了建立逻辑,没有return清理函数。
修法:在 effect 中return () => { ... },在下次 effect 执行前和卸载时清理上一次副作用。现象:报错
React Hook ... is called conditionally。
原因:Hook 被写在if、for或嵌套函数中,导致每次渲染调用顺序不一致。
修法:所有 Hook 移到组件顶层调用,条件判断放到 Hook 内部或调用之后。现象:子组件通过 Context 拿不到数据或直接报错。
原因:提供方和消费方导入了不同的 context 对象。
修法:单独创建并导出同一个 context 文件,所有组件统一从该文件导入。现象:点击切换任务后刷新丢失,或点击操作没有发请求。
原因:把用户事件触发的请求误放进useEffect,或只改本地状态未调接口。
修法:事件触发的请求直接写在事件处理函数中;useEffect只处理挂载、更新、卸载相关副作用。
练习
- 在
ChatRoom中用一个useEffect完成:挂载时连接默认房间、roomId变化时先断开旧连接再连接新房间、卸载时断开连接,并打印执行顺序。 - 用
useEffect+ 内部async函数请求data.json,把结果存入 state 并渲染列表;说明为什么不能直接把 effect 回调写成async。 - 给一段在 effect 中使用
count、age的代码补齐依赖项,并验证空数组时会出现什么现象。 - 将频道数据通过 Context 从
App共享给Home和ChannelModal,要求单独创建 context 文件,并过滤出myChannels与moreChannels。 - 找出一段在
if中调用useState/useEffect的代码,改为顶层调用,并保持原有业务逻辑等价。
课堂画面






原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | useEffect的使用 | 13:56 |
| 2 | useEffect的扩展 | 16:18 |
| 3 | useEffect应用-发送请求 | 13:08 |
| 4 | ReactHooks解释和使用规则 | 06:20 |
| 5 | 知乎频道管理案例-1展示或隐藏频道弹窗 | 09:07 |
| 6 | 知乎频道管理案例-2渲染频道数据 | 16:32 |
| 7 | 知乎频道管理案例-3移除或添加频道 | 16:38 |
小结
useEffect用于处理组件生命周期相关副作用,依赖数组决定执行时机。- 空数组通常表示仅挂载执行;包含可变依赖表示挂载及依赖变化时执行。
- 清理函数会在下一次 effect 执行前和组件卸载时执行。
- 推荐一个
useEffect负责一个完整功能,把建立与清理逻辑放在一起。 - 发请求时 effect 外层保持同步,内部创建
async函数并调用。 - 只有生命周期相关请求放
useEffect,用户点击等事件请求放事件处理函数。 - Hooks 必须在组件顶层按稳定顺序调用,不能放进条件、循环或嵌套函数。
- 跨组件共享 Context 时,必须保证提供方和消费方使用同一个 context 对象。