Skip to content

useContext与pubsub(Day7 3/3) ​

本页对应 React + TypeScript + 医通项目,共 7 段视频,约 1 小时 29 分。

学习目标 ​

  1. 能独立创建并配置 axios request 实例,设置 baseURL、timeout,注册请求/响应拦截器,并接入 NProgress 实现请求进度显示与关闭。
  2. 能依据接口文档或直接请求结果,准确提取请求方式、URL、请求参数(query/path/body)及响应结构,并据此选择 axios 方法与参数携带位置。
  3. 能按“查看接口→封装 API 请求函数→在生命周期钩子或用户交互中调用→定义状态并渲染页面”的套路完成数据请求功能。
  4. 能在 useEffect 中避免直接传入 async 回调,改为内部定义 async 函数并调用,消除控制台警告。
  5. 能使用 PubSub 完成兄弟组件通信,将 Header 中的搜索关键字发布给 Main 组件并正确订阅/取消订阅。
  6. 能实现 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-js
js
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;订阅回调里更新状态要确认组件仍挂载;消息名、数据名、请求参数名要统一。

易错点小结 ​

  1. PubSub.subscribe 返回消息 id,取消订阅时优先用它。
  2. 回调参数是 (msg, data),不是 (data)。
  3. 发布和订阅的消息名必须一致。
  4. 挂载订阅、卸载取消,成对出现。
  5. useEffect 回调不要直接 async,要单独定义 async 函数再调用。
  6. 不要忘记 import PubSub from 'pubsub-js'。
  7. 取消所有订阅 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 都是条件渲染状态,必须与请求过程同步切换。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一:useContext —— 祖先向任意后代通信 ​

src/context.js

js
// 1. 导入 createContext 函数
import { createContext } from 'react'
// 2. 创建 context 对象
const context = createContext()
// 3. 将 context 对象暴露出去
export default context

src/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 request

src/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 的依赖数组写 [],表示组件挂载后只请求一次(首屏渲染),不写会反复请求。

常见错误 ​

  1. 现象:Provider 已设置 value,子组件 useContext 仍拿到默认值/undefined。→ 原因:Provider 未真正包住消费组件,或子组件导入了另一个 context 实例。→ 修法:统一从同一模块导入 context;检查组件树包裹关系;给 context 合理默认值。
  2. 现象:useEffect 回调直接写成 async,控制台出现 “must not return anything besides a function” 警告。→ 原因:async 回调返回 Promise,不符合 effect 清理函数约定。→ 修法:在 effect 内定义 async 函数并立即调用,或使用 .then,不要在 effect 回调本身加 async。
  3. 现象:PubSub 订阅后页面切换仍触发回调、请求重复、出现卸载后 setState。→ 原因:订阅未取消,或取消时传错消息名/id。→ 修法:保存 subscribe 返回的 id,在 effect cleanup 中按 id 取消订阅;消息名统一用常量。
  4. 现象:请求拦截器配置后请求异常、headers 丢失、请求不按预期发出。→ 原因:请求拦截器成功回调未 return config。→ 修法:成功分支必须返回 config;错误分支返回 Promise.reject(error)。
  5. 现象:响应拦截器已返回 response.data,组件里再取 .data 得到 undefined。→ 原因:拦截器简化了响应结构,组件仍按原始 axios 响应取数。→ 修法:团队统一响应结构约定;组件按拦截器后的结构取数,或取消拦截器简化。
  6. 现象:GET 请求后端收不到查询参数,POST 请求后端收不到请求体。→ 原因:query 参数放进了 data,请求体参数放进了 params。→ 修法:GET/DELETE 查询参数用 params;POST/PUT/PATCH 请求体用 data;路径参数拼到 URL。
  7. 现象:页面一直 loading,请求失败后也不关闭;或错误被吞掉无法定位。→ 原因:loading 只在成功分支关闭;响应错误分支返回了永远 pending 的 Promise。→ 修法:用 try/finally 关闭 loading;错误统一处理后 reject,不要用空 pending Promise 吞错。
  8. 现象:axios.create 配了拦截器,但请求没走 token/loading。→ 原因:组件仍直接使用全局 axios,没有使用封装后的 request 实例。→ 修法:统一从 request 模块导入实例;API 层只调用该实例,禁止组件散落全局 axios。

练习 ​

  1. 用 useContext 重构一个三层组件传值案例,并增加“修改 username”按钮;要求检查 Provider value 的引用稳定性,说明哪些组件会重渲染。
  2. 用 PubSub 实现 Header 搜索框向 List 组件发送 keyword;要求消息名常量化、订阅保存 id、cleanup 取消订阅,并验证连续搜索不会重复订阅。
  3. 封装一个 request 实例:配置 baseURL、timeout、请求拦截携带 token、NProgress start/done、响应简化 data、401 清理 token 并跳登录、错误统一 reject。
  4. 实现 GitHub 用户搜索:输入关键字点击搜索,loading 条件渲染,列表渲染头像、用户名、主页链接;补齐空结果、请求失败、首次进入页面三种状态。
  5. 排查并修复一个“Provider 已设 value 但 useContext 拿不到”的 bug;写出排查清单,至少覆盖 context 实例、导入路径、Provider 包裹范围、默认值四项。

原视频对照 ​

#视频时长
1react-request-配置11:17
2axios-repo-案例练习19:11
3axios发送请求开发思路05:31
4axios-查看请求接口-如何携带参数-具体操作思路08:59
5axios-发送请求开发套路应用15:22
6github-users静态页面布局09:09
7github-users-功能实现19:47

小结 ​

  1. Context 适合跨层级、低频共享数据;高频变化时优先关注 value 引用稳定,避免无意义重渲染。
  2. 订阅通信必须成对管理:订阅保存 id、卸载取消订阅、消息名常量化,避免重复订阅和误取消。
  3. 项目中的 axios 应走“实例 + 拦截器 + API 层”的路线,组件只调用 API 函数,不直接散落请求细节。
  4. 请求状态要收敛:loading 用 try/finally 关闭,错误统一处理,不能让 Promise 永久 pending。
  5. useEffect 中处理异步要包一层,并明确依赖数组;搜索、分页类请求要考虑竞态和清理。
  6. 参数位置按 axios 语义放置:query 用 params,body 用 data,path 拼 URL;响应结构一旦简化,全项目统一取数约定。
  7. 页面开发先做主流程,再补 loading、空态、错误态、首次态;条件渲染由状态驱动,状态命名要表达业务含义。
  8. 调试顺序建议固定:先看 Network 的请求方式/URL/参数/响应,再看拦截器返回值,最后查组件状态与渲染。