主题
useContext与pubsub(Day7 3/3)
本页对应 React + TypeScript + 医通项目,共 7 段视频,约 1 小时 29 分。
学习目标
- 能独立创建并配置 axios request 实例,设置 baseURL、timeout,注册请求/响应拦截器,并接入 NProgress 实现请求进度显示与关闭。
- 能依据接口文档或直接请求结果,准确提取请求方式、URL、请求参数(query/path/body)及响应结构,并据此选择 axios 方法与参数携带位置。
- 能按“查看接口→封装 API 请求函数→在生命周期钩子或用户交互中调用→定义状态并渲染页面”的套路完成数据请求功能。
- 能在 useEffect 中避免直接传入 async 回调,改为内部定义 async 函数并调用,消除控制台警告。
- 能使用 PubSub 完成兄弟组件通信,将 Header 中的搜索关键字发布给 Main 组件并正确订阅/取消订阅。
- 能实现 GitHub Users/Repos 案例:拆分 Header/Main 静态布局、列表渲染、loading 条件渲染、首次进入页面提示及搜索后状态切换。
知识点
一、为什么需要 useContext
组件通信里,最基础的方式是父传子:父组件通过 props 把数据传给子组件。但如果层级很深,比如 App → Father → Son,App 的数据要传到 Son,就要一层层写 props。中间组件即使不用数据,也不得不帮忙转发,代码会变得啰嗦,维护也麻烦。
useContext 解决的就是这个问题:祖先组件向任意后代组件通信。它不要求中间每一层都写 props,而是由祖先通过 Provider 提供数据,后代在需要的地方直接用 useContext 取。
**怎么想:**先判断数据是不是“跨多层、多个后代都要用”。如果是,就可以考虑 Context。**怎么做:**创建 context 对象 → 用 Provider 包裹后代并绑定 value → 后代用 useContext 读取。**易错点:**Context 不是替代所有 props;只在确实需要跨层共享时使用,否则普通 props 更直观。
二、创建 context 对象
**为什么需要:**后代要读取同一份上下文,必须先有一个共同的 context 对象。
**怎么想:**把创建 context 的代码单独放到 src/context.js,方便各个组件导入同一个对象。
怎么做:
js
// src/context.js
// 1 导入createContext 函数
import {createContext} from 'react'
// 2 创建context对象
const context = createContext();
// 3. 将context对象暴露出去
export default context;**易错点:**不要在每个组件里分别 createContext(),否则它们不是同一个上下文;导出方式要和导入方式一致,这里用的是默认导出。
三、用 Provider 包裹后代并绑定 value
**为什么需要:**只有被 Provider 包裹的后代,才能拿到这个 context 提供的数据。
**怎么想:**App 是祖先,Father、Son 是后代,所以在 App 里用 context.Provider 包住后代,并把要共享的数据放到 value 上。
怎么做:
jsx
import React from 'react'
// 4. 导入context对象
import context from './context'
import Father from './components/Father'
export default function App() {
return (
// 5. 使用Provider包裹 后代组件,被包裹的后代组件及其所有的子组件,都可以直接使用 value中的数据
<context.Provider value={{username:'atguigu', age:10}}>
<div>
<h3>App</h3>
<Father />
</div>
</context.Provider>
)
}易错点:Provider 必须包住消费者;value 是固定属性名,不要写成 data 或 val;如果 value 是对象,每次渲染新建对象可能引起消费者重新渲染,实际项目中要留意。
四、后代用 useContext 获取数据
**为什么需要:**后代组件需要把 context 里的 value 读出来,参与渲染。
**怎么想:**Son 是后代,先导入同一个 context 对象,再调用 useContext(context),返回值就是 Provider 绑定的 value。
怎么做:
jsx
import React, { useContext } from 'react'
// 6. 导入context对象
import context from '../context'
export default function Son() {
// 7. 使用useContext hook函数,解析出context对象中绑定的value数据
// let data = useContext(context)
// console.log('data:', data);
let {username, age} = useContext(context)
return (
<div>
<p>username: {username}</p>
<p>age: {age}</p>
</div>
)
}易错点:useContext 只能在函数组件或自定义 Hook 中调用;导入路径要指向同一个 context.js;如果取不到值,先检查 Son 是否真的在 Provider 内部,以及 value 是否绑定正确。
五、调试顺序与转写稿说明
调试时按笔记顺序排查:先确认 context.js 已创建并默认导出;再确认 App 中 context.Provider 包住了 Father;最后确认 Son 导入的是同一个 context,并用 useContext(context) 解构。若页面显示空值,优先检查“是否在 Provider 内”和“value 是否写对”。
需要说明:本节转写稿主要讲 axios 请求配置、拦截器、GitHub 案例和 loading 条件渲染,同音字如“exse/Excus”应理解为 axios,“楼顶/老年”应理解为 loading,“怕bus/趴不”应理解为 pubsub;这些内容与本节 useContext 没有直接代码对应,因此不强行混入本节推导,避免编造。本节笔记未包含图片,故不插入图片。
为什么需要 PubSub:兄弟组件通信的痛点
为什么需要:在 React 里,如果 Header 组件里拿到用户输入的搜索关键字,而真正要发请求、渲染列表的是 Main 组件,这两个组件就是兄弟关系。父组件传 props 可以通信,但兄弟之间直接传并不方便;如果为了一个关键字层层传 props,代码会变绕。
怎么想:PubSub 是“发布—订阅”模式。谁产生数据,谁就“发布消息”;谁需要数据,谁就“订阅消息”。发布者和订阅者不用互相认识,只认同一个“消息名”,从而解耦。
怎么做:先安装并导入:
bash
npm i pubsub-jsjs
import PubSub from 'pubsub-js'易错点:pub 是 publish(发布),sub 是 subscribe(订阅),不要写成“pa up”“怕不”等;包名是 pubsub-js,导入的默认对象是 PubSub。
订阅消息:谁接收数据,谁订阅
为什么需要:Main 组件需要接收 Header 传来的搜索关键字,所以它要订阅。
怎么想:订阅要发生在组件挂载阶段,这样消息来了才能收到;同时订阅会返回一个消息 id,后面取消订阅要用。
怎么做:
js
let 消息id = PubSub.subscribe('消息名', function(msg, data){ })在 React 函数组件里,通常放在 useEffect 中,并且用空数组表示只订阅一次:
js
useEffect(() => {
const token = PubSub.subscribe('search', (msg, userName) => {
// msg 是消息名,userName 是发布时传过来的数据
console.log(msg, userName)
})
return () => {
PubSub.unsubscribe(token)
}
}, [])易错点:回调第一个参数是消息名 msg,第二个才是数据 data,不要把第一个参数当数据用;消息名必须和发布时完全一致。
发布消息:谁产生数据,谁发布
为什么需要:Header 点击搜索按钮后,要把输入框里的用户名发出去。
怎么想:先获取输入框内容,再调用 publish 发布,消息名要和订阅端一致。
怎么做:
js
PubSub.publish('消息名', 数据)例如 Header 中点击搜索:
js
const userName = inputRef.current.value.trim()
PubSub.publish('search', userName)易错点:发布的数据放在第二个参数;如果发布时消息名写错,订阅端收不到;空字符串最好先判断再发布。
取消订阅:组件卸载时必须清理
为什么需要:组件卸载后如果还保留订阅,可能造成内存泄漏或重复执行回调。
怎么想:订阅和取消订阅要成对出现。React 中在 useEffect 的清理函数里取消最稳。
怎么做:
js
PubSub.unsubscribe(消息id) // 取消该消息id的订阅
PubSub.unsubscribe('消息名') // 取消该消息名所有的订阅
PubSub.clearAllSubscriptions() // 取消所有消息实际组件里推荐:
js
useEffect(() => {
const token = PubSub.subscribe('search', handler)
return () => PubSub.unsubscribe(token)
}, [])易错点:不要只订阅不取消;unsubscribe(消息id) 和 unsubscribe(消息名) 参数不同,别混;clearAllSubscriptions() 会清掉所有订阅,项目里慎用。
在 React 生命周期中的使用方式
为什么需要:订阅太早或太晚都不合适。组件还没挂载就订阅,回调里可能拿不到最新状态;组件卸载后不取消,会留下隐患。
怎么想:按笔记要求——挂载阶段订阅,即将卸载阶段取消订阅。函数组件用 useEffect 模拟:空依赖执行一次,返回清理函数。
怎么做:
js
useEffect(() => {
const token = PubSub.subscribe('search', (msg, userName) => {
// 收到用户名后发请求
})
return () => {
PubSub.unsubscribe(token)
}
}, [])易错点:useEffect 回调不能直接写成 async。转写稿里就出现过警告:useEffect 回调返回了 Promise。正确做法是单独定义 async 函数再调用:
js
useEffect(() => {
const token = PubSub.subscribe('search', async (msg, userName) => {
// 可以在这里调用 async 请求函数
})
return () => PubSub.unsubscribe(token)
}, [])如果需要在 effect 内直接发请求,应写成:
js
useEffect(() => {
const fetchData = async () => {
// await ...
}
fetchData()
}, [])案例串联:Header 搜索,Main 接收并请求
为什么需要:转写稿中的 GitHub Users 案例里,Header 负责输入和点击搜索,Main 负责展示用户列表。数据要在 Main 用,所以请求更适合放 Main。
怎么想:Header 只负责“发布关键字”,Main 订阅后拿到 userName,再发请求并设置列表状态。
怎么做:
js
// Header
const handleSearch = () => {
const userName = inputRef.current.value.trim()
PubSub.publish('search', userName)
}
// Main
useEffect(() => {
const token = PubSub.subscribe('search', async (msg, userName) => {
const res = await getUsers(userName)
setUsers(res.items)
})
return () => PubSub.unsubscribe(token)
}, [])易错点:不要因为“点击后才发请求”就把请求硬塞在 Header;订阅回调里更新状态要确认组件仍挂载;消息名、数据名、请求参数名要统一。
易错点小结
PubSub.subscribe返回消息 id,取消订阅时优先用它。- 回调参数是
(msg, data),不是(data)。 - 发布和订阅的消息名必须一致。
- 挂载订阅、卸载取消,成对出现。
useEffect回调不要直接async,要单独定义 async 函数再调用。- 不要忘记
import PubSub from 'pubsub-js'。 - 取消所有订阅
PubSub.clearAllSubscriptions()影响全局,慎用。
一、先配置再使用:axios 的基础封装
为什么需要:如果在每个组件里都写完整地址,路径重复、超时和 token 处理也会散落各处。转写稿强调:项目里不是直接拿 axios 发请求,而是“先配置再使用”。
怎么想:一般建 request/index.js,用 axios.create 配 baseURL、timeout,再挂请求/响应拦截器,最后导出配置好的 request。
怎么做:
js
const request = axios.create({
baseURL: 'http://localhost:8080',
timeout: 2000
})笔记还给了 defaults 写法:axios.defaults.baseURL = 'xxx.com:8080'。区别是:defaults 只能配一个基础路径;create 可以创建多个对象,配置多个基础路径。request 是简化版 axios 对象,没有 cancelToken、all 方法。
易错点:不要一会儿用 axios、一会儿用 request,否则拦截器不生效;基础路径末尾和接口路径开头的斜杠要统一。
二、请求拦截器与响应拦截器
为什么需要:请求前要带公共参数、开 loading;响应后要关 loading、简化数据、统一处理鉴权和错误。
怎么做:请求拦截器里从 localStorage 取 token,写入请求头,并 NProgress.start():
js
request.interceptors.request.use(config => {
let token = localStorage.getItem('token')
if (token) config.headers.token = token
NProgress.start()
return config
})响应拦截器里关 loading、返回 response.data,并处理 401 和错误:
js
request.interceptors.response.use(response => {
if (response.statusCode === 401) {
localStorage.removeItem('token')
location.href = '/login.html'
}
NProgress.done()
return response.data
}, error => {
console.log('error:', error)
return new Promise(() => {})
})转写稿补了 NProgress 的使用:安装后既要导入 JS,也要导入 CSS,否则进度条样式不生效。
易错点:start 和 done 要配对;响应拦截器返回 response.data 后,组件里不要再取 .data;错误处理返回 pending Promise 是为了阻止 Promise 链继续向下传递。
三、axios 的两种请求写法
为什么需要:不同接口参数位置不同,必须分清 query、路径参数和请求体。
怎么做:函数式写法主要看 config:
js
axios({
method: 'get',
url: 'zyx.com',
params: { a: 1, b: 2 }
}) // zyx.com?a=1&b=2
axios({
method: 'post',
url: 'zyx.com',
data: { username: 'zyx', age: 20 }
})对象式写法分两类:get/delete 是 axios.get(url[, config])、axios.delete(url[, config]);post/put/patch 是 axios.post(url, data[, config]) 等。
易错点:params 是 query 参数,data 是请求体;路径参数要直接拼在 URL 里;不要把 query 参数误塞进 data。
四、React 中的调用时机与 useEffect
为什么需要:首屏渲染和用户交互的请求时机不同。笔记明确:首屏数据渲染放在 componentDidMount;用户交互则在事件回调中调用。
怎么做:函数组件用 useEffect 模拟挂载:
js
useEffect(() => {
const load = async () => {
const res = await getReple('vue')
console.log(res)
}
load()
}, [])易错点:转写稿反复提醒:useEffect 的回调不能直接写成 async,否则会返回 Promise 并触发警告。应单独定义 async 函数再手动调用。若用 pubsub 做兄弟组件通信,订阅后要在清理函数里取消订阅。
五、案例套路:查看接口 → 封装函数 → 调用 → 渲染
为什么需要:避免上来就写 JSX。转写稿给出固定开发思路:查看 API 的请求方式、URL、参数、响应内容;封装 API 请求函数;在生命周期或交互中调用;定义状态渲染页面。
怎么做:在 api/github.js 中封装:
js
export function getReple(keyword, starType = 'star') {
return request.get('/search/repositories', {
params: { q: keyword, sort: starType }
})
}
export function getUsers(userName) {
return request.get('/search/users', {
params: { q: userName }
})
}组件里先实现核心列表,再加 loading 和首次提示。loading 的套路是:请求前 setLoading(true),数据回来后 setLoading(false);首次提示用 first 状态控制,搜索后设为 false。
易错点:初始状态若给 null,渲染时读 reple.html_url 会报错,可先给空对象;列表渲染要取 items[0] 或 map;列表项加 key;loading 和 first 都是条件渲染状态,必须与请求过程同步切换。
课堂画面






代码示例
示例一:useContext —— 祖先向任意后代通信
src/context.js
js
// 1. 导入 createContext 函数
import { createContext } from 'react'
// 2. 创建 context 对象
const context = createContext()
// 3. 将 context 对象暴露出去
export default contextsrc/App.jsx
jsx
import React from 'react'
// 4. 导入 context 对象
import context from './context'
import Father from './components/Father'
export default function App() {
return (
// 5. 用 Provider 包裹后代组件,被包裹的组件及其所有子组件都能直接使用 value 中的数据
<context.Provider value={{ username: 'atguigu', age: 10 }}>
<div>
<h3>App</h3>
<Father />
</div>
</context.Provider>
)
}src/components/Father.jsx
jsx
import React from 'react'
import Son from './Son'
export default function Father() {
return (
<div>
<h3>Father</h3>
<Son />
</div>
)
}src/components/Son.jsx
jsx
import React, { useContext } from 'react'
// 6. 导入 context 对象
import context from '../context'
export default function Son() {
// 7. 使用 useContext 解析 context 对象中绑定的 value 数据
// let data = useContext(context)
// console.log('data:', data)
let { username, age } = useContext(context)
return (
<div>
<p>username: {username}</p>
<p>age: {age}</p>
</div>
)
}关键行与易错点
React.createContext()只在模块里创建一次,不要在组件函数体内创建,否则每次渲染都是新的 context 对象。- Provider 的
value一定要绑定数据,<context.Provider>不写value,后代拿到的是创建时的默认值(本例为undefined)。 useContext(context)必须写在函数组件(或自定义 Hook)内部的最外层,不能放在if、循环、回调里——Hook 规则。- 只有被 Provider 包裹的后代才能拿到值;中间组件
Father完全不需要透传 props,这正是 useContext 解决“跨层级通信”的意义。 - 解构时属性名要和
value中的键一致:{ username, age },写成name会得到undefined。
示例二:pubsub —— 兄弟组件通信(Header 发布,Main 订阅)
src/components/Header.jsx
jsx
import React, { useRef } from 'react'
import PubSub from 'pubsub-js'
export default function Header() {
const inputRef = useRef(null)
function handleSearch() {
const username = inputRef.current.value.trim()
// 发布消息:消息名 'search',携带数据 username
PubSub.publish('search', username)
}
return (
<div>
<input ref={inputRef} placeholder="请输入用户名搜索" />
<button onClick={handleSearch}>search</button>
</div>
)
}src/components/Main.jsx
jsx
import React, { useEffect, useState } from 'react'
import PubSub from 'pubsub-js'
export default function Main() {
const [username, setUsername] = useState('')
useEffect(() => {
// 订阅消息,返回消息 id
const token = PubSub.subscribe('search', (msg, data) => {
console.log('msg:', msg) // 消息名 'search'
console.log('data:', data) // 发布时携带的数据
setUsername(data)
})
// 组件即将卸载时取消订阅
return () => {
PubSub.unsubscribe(token)
}
}, [])
return <div>收到搜索关键字:{username}</div>
}关键行与易错点
- 回调的第一个参数是消息名,第二个才是数据,写成
(data) => ...拿到的是'search'而不是关键字,这是最常见的错误。 PubSub.subscribe返回的是消息 id(token),取消订阅要传这个 token:PubSub.unsubscribe(token);传消息名会取消该消息名的所有订阅。- 订阅写在
useEffect中、取消订阅写在useEffect的返回函数中,依赖数组给[],保证“挂载订阅、卸载取消”各执行一次。 - 订阅与发布必须使用完全相同的消息名,一个
search一个Search就永远收不到。 - 谁接收数据谁订阅,谁产生数据谁发布——本例 Header 只是发消息,不接收,所以 Header 里不写 subscribe。
示例三:axios 封装 + 请求 + loading 条件渲染
src/request/index.js
js
import axios from 'axios'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
// 基础配置:baseURL、timeout
const request = axios.create({
baseURL: 'https://api.github.com',
timeout: 20000
})
// 请求拦截器
request.interceptors.request.use(config => {
// 1. 请求头携带公共参数 token
const token = localStorage.getItem('token')
if (token) {
config.headers.token = token
}
// 2. 开启 loading 效果
NProgress.start()
return config
})
// 响应拦截器
request.interceptors.response.use(
response => {
NProgress.done() // 关闭 loading
return response.data // 简化响应数据
},
error => {
NProgress.done()
console.log('error: ', error)
// 阻止 promise 链条向下传递
return new Promise(() => {})
}
)
export default requestsrc/api/github.js
js
import request from '../request'
// 获取最受欢迎的 repo
export function getRepo(keyword, sort = 'stars') {
return request.get('/search/repositories', {
params: { q: keyword, sort } // query 参数用 params
})
}
// 根据用户名获取用户列表
export function getUsers(username) {
return request.get('/search/users', {
params: { q: username }
})
}src/App.jsx
jsx
import React, { useEffect, useState } from 'react'
import { getRepo } from './api/github'
export default function App() {
const [loading, setLoading] = useState(false) // 请求中的标识符
const [repo, setRepo] = useState({}) // 仓库对象,初始给空对象
useEffect(() => {
// 注意:useEffect 的回调不能直接写 async,要单独定义 async 函数再调用
async function fetchData() {
setLoading(true) // 发送请求前开启 loading
const res = await getRepo('vue')
setRepo(res.items[0]) // 取 items 第 0 条
setLoading(false) // 数据回来关闭 loading
}
fetchData()
}, [])
if (loading) return <h2>loading...</h2>
return (
<div>
<h3>Most star repo is</h3>
<a href={repo.html_url}>{repo.name}</a>
</div>
)
}关键行与易错点
- 配置与使用分离:先在
request/index.js里axios.create配好 baseURL、timeout、拦截器,组件里只import request,不再写完整路径。 - 响应拦截器里
return response.data已经简化过数据,调用处不能再写res.data.items,直接用res.items。 params是 query 参数(拼到 URL 上),data是请求体;get 请求的参数放params,放错位置接口取不到值。- 错误处理里
return new Promise(() => {})是为了中断 promise 链,避免业务代码继续往下走。 useEffect回调不能写成async,否则返回 Promise 会报 “useEffect must not return anything besides a function” 警告,必须像上面那样定义内部 async 函数再调用。useState({})初始值一定要给空对象,给undefined时首屏读repo.html_url会直接报错(空对象读属性只是undefined,不报错)。- loading 的套路固定:请求前
setLoading(true),数据回来后setLoading(false),中间用if (loading) return <h2>loading...</h2>做条件渲染。 useEffect的依赖数组写[],表示组件挂载后只请求一次(首屏渲染),不写会反复请求。
常见错误
- 现象:Provider 已设置 value,子组件
useContext仍拿到默认值/undefined。→ 原因:Provider 未真正包住消费组件,或子组件导入了另一个 context 实例。→ 修法:统一从同一模块导入 context;检查组件树包裹关系;给 context 合理默认值。 - 现象:
useEffect回调直接写成 async,控制台出现 “must not return anything besides a function” 警告。→ 原因:async 回调返回 Promise,不符合 effect 清理函数约定。→ 修法:在 effect 内定义 async 函数并立即调用,或使用.then,不要在 effect 回调本身加 async。 - 现象:PubSub 订阅后页面切换仍触发回调、请求重复、出现卸载后 setState。→ 原因:订阅未取消,或取消时传错消息名/id。→ 修法:保存
subscribe返回的 id,在 effect cleanup 中按 id 取消订阅;消息名统一用常量。 - 现象:请求拦截器配置后请求异常、headers 丢失、请求不按预期发出。→ 原因:请求拦截器成功回调未
return config。→ 修法:成功分支必须返回 config;错误分支返回Promise.reject(error)。 - 现象:响应拦截器已返回
response.data,组件里再取.data得到 undefined。→ 原因:拦截器简化了响应结构,组件仍按原始 axios 响应取数。→ 修法:团队统一响应结构约定;组件按拦截器后的结构取数,或取消拦截器简化。 - 现象:GET 请求后端收不到查询参数,POST 请求后端收不到请求体。→ 原因:query 参数放进了
data,请求体参数放进了params。→ 修法:GET/DELETE 查询参数用params;POST/PUT/PATCH 请求体用data;路径参数拼到 URL。 - 现象:页面一直 loading,请求失败后也不关闭;或错误被吞掉无法定位。→ 原因:loading 只在成功分支关闭;响应错误分支返回了永远 pending 的 Promise。→ 修法:用
try/finally关闭 loading;错误统一处理后reject,不要用空 pending Promise 吞错。 - 现象:
axios.create配了拦截器,但请求没走 token/loading。→ 原因:组件仍直接使用全局axios,没有使用封装后的 request 实例。→ 修法:统一从 request 模块导入实例;API 层只调用该实例,禁止组件散落全局 axios。
练习
- 用
useContext重构一个三层组件传值案例,并增加“修改 username”按钮;要求检查 Provider value 的引用稳定性,说明哪些组件会重渲染。 - 用 PubSub 实现 Header 搜索框向 List 组件发送 keyword;要求消息名常量化、订阅保存 id、cleanup 取消订阅,并验证连续搜索不会重复订阅。
- 封装一个 request 实例:配置 baseURL、timeout、请求拦截携带 token、NProgress start/done、响应简化 data、401 清理 token 并跳登录、错误统一 reject。
- 实现 GitHub 用户搜索:输入关键字点击搜索,loading 条件渲染,列表渲染头像、用户名、主页链接;补齐空结果、请求失败、首次进入页面三种状态。
- 排查并修复一个“Provider 已设 value 但 useContext 拿不到”的 bug;写出排查清单,至少覆盖 context 实例、导入路径、Provider 包裹范围、默认值四项。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | react-request-配置 | 11:17 |
| 2 | axios-repo-案例练习 | 19:11 |
| 3 | axios发送请求开发思路 | 05:31 |
| 4 | axios-查看请求接口-如何携带参数-具体操作思路 | 08:59 |
| 5 | axios-发送请求开发套路应用 | 15:22 |
| 6 | github-users静态页面布局 | 09:09 |
| 7 | github-users-功能实现 | 19:47 |
小结
- Context 适合跨层级、低频共享数据;高频变化时优先关注 value 引用稳定,避免无意义重渲染。
- 订阅通信必须成对管理:订阅保存 id、卸载取消订阅、消息名常量化,避免重复订阅和误取消。
- 项目中的 axios 应走“实例 + 拦截器 + API 层”的路线,组件只调用 API 函数,不直接散落请求细节。
- 请求状态要收敛:loading 用
try/finally关闭,错误统一处理,不能让 Promise 永久 pending。 useEffect中处理异步要包一层,并明确依赖数组;搜索、分页类请求要考虑竞态和清理。- 参数位置按 axios 语义放置:query 用
params,body 用data,path 拼 URL;响应结构一旦简化,全项目统一取数约定。 - 页面开发先做主流程,再补 loading、空态、错误态、首次态;条件渲染由状态驱动,状态命名要表达业务含义。
- 调试顺序建议固定:先看 Network 的请求方式/URL/参数/响应,再看拦截器返回值,最后查组件状态与渲染。