Skip to content

hook函数的使用原则与自定义hook(Day7 2/3) ​

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

学习目标 ​

  • 能够用图示或文字准确说明 props 在父传子、子传父、祖先传后代、兄弟组件通信中的传递路径,并指出其逐级传递的局限。
  • 能够使用 createContext、Provider 和 useContext 完成祖先组件向任意后代组件跨级传值,并验证中间组件无需中转。
  • 能够使用 pubsub 的订阅、发布、按消息 ID 取消订阅、按消息名取消订阅和清空订阅 API,完成任意组件间通信并在组件卸载前正确取消订阅。
  • 能够使用 pubsub 改写 TodoList 的添加和删除功能,并说明在 useEffect 中直接使用状态导致闭包问题时,如何用 setState 函数式更新解决。
  • 能够完成 axios 的 baseURL、timeout、请求/响应拦截器基础配置,并正确使用函数式与对象式语法发送 GET、POST、PUT、DELETE 等请求。

知识点 ​

先分清:Hook 函数与普通函数的命名边界 ​

为什么需要: React 要在函数组件渲染过程中识别“哪些函数是 Hook、哪些只是普通工具函数”。如果命名混乱,开发者容易把 useState、useRef 这类 Hook 放进普通函数里调用,最后得到“Invalid hook call”或 ESLint 报错。

怎么想: 笔记里说得很直接:Hook 和普通函数的区别首先在函数名——以 use 开头,后面接首字母大写的单词,React 就按 Hook 函数来识别。例如 usePosition 是自定义 Hook;App、Father 是函数组件;fn、save 是普通函数。命名不是装饰,而是告诉 React 和开发者:这个函数内部可能调用其他 Hook。

怎么做: 自定义 Hook 写成 useXxx,内部可以调用 useState、useRef 等 Hook;函数组件名首字母大写;普通工具函数不要随意用 use 开头,除非它确实是自定义 Hook。

易错点: 不要以为“名字写成 useSave 就自动安全”。名字只是约定,真正合法与否还取决于调用位置。转写稿里反复强调“先看报错再改”,本节调试也应如此:把 useState 放进 save,控制台会提示它既不是 React 函数组件,也不是自定义 Hook;改成 useSave 后,才符合“自定义 Hook 内部调用其他 Hook”的写法。

原则一:Hook 只能在函数组件中使用 ​

为什么需要: Hook 依赖函数组件的渲染顺序来保存状态。类组件用的是 this.state、生命周期和实例机制,和 Hook 不是一套模型。

怎么想: 看到 class Xxx extends React.Component,就不要在里面写 useState、useEffect。类组件继续用 setState 和生命周期;函数组件才用 Hook。

怎么做: 把需要状态的 UI 写成函数组件,在组件顶层调用 Hook;类组件不要混用 Hook。

易错点: 不要因为“类组件里也能写函数”就把 Hook 塞进类方法。Hook 不是普通工具函数,它必须挂在 React 的函数组件渲染链路上。

原则二:Hook 只能在其他 Hook 中使用 ​

为什么需要: 普通函数不一定在 React 渲染期间执行,React 无法把其中的 useState 关联到某个组件实例。笔记代码里注释掉的 save 就是反例:save 是普通函数,内部调用 useState 会报错。

怎么想: 如果一段逻辑需要复用 Hook,就把这段逻辑封装成自定义 Hook,而不是封装成普通函数。笔记里把 save 改成 useSave,就是为了让“普通函数”变成“自定义 Hook”。

怎么做: 写成 function useSave(){ let [count, setCount] = useState(100) },再在函数组件或其他自定义 Hook 的顶层调用它。

易错点: 自定义 Hook 也不能在事件回调里随便调用。笔记代码最后写了 onClick={useSave},这只能用于展示命名,不是正确用法:点击事件是普通回调,在里面调用 useSave 仍然违反 Hook 规则。正确思路是把 useState 放在组件顶层,事件里只调用普通函数。

原则三:Hook 数量必须确定,不能写在 if 或 for 中 ​

为什么需要: React 靠 Hook 的调用顺序来匹配状态。如果这次渲染调用了 1 个 useState,下次因为 if 变成 0 个,或因为 for 变成 100 个,顺序就乱了。

怎么想: Hook 调用必须稳定:每次渲染都按同样顺序、同样数量执行。条件逻辑应放在 Hook 内部,或放在 Hook 返回值之后,而不是决定“要不要调用 Hook”。

怎么做: 把 useState、useRef、useEffect 都放在组件顶层。笔记里的 const usernameRef = useRef() 是合法的;if(true){ useState(...) } 和 for(...){ useState(...) } 都是反例。

易错点: 不要用“条件成立才创建状态”的写法。需要条件时,可以创建状态后再根据条件渲染;不要用 if、for、while 包裹 Hook。调试时把笔记里的 if、for 取消注释,控制台会提示 Hook 调用不符合规则;恢复注释后,组件顶层只保留稳定的 Hook 调用。

代码逐段解读与调试 ​

以下代码保留笔记原样,其中注释掉的报错代码用于对比:

jsx
import React, { useRef, useState } from 'react'

export default function FunTest() {
    const usernameRef = useRef();
    // function save(){
    //     // 报错 :React Hook "useState" is called in function "save" that is neither a React function component nor a custom React Hook function. 
    //     //   React Hook“useState”在函数“save”中调用,该函数既不是React函数组件,也不是自定义React Hook函数。
    //     let [count, setCount] = useState(100)
    // }

    function useSave(){ // useSave 是自定义的hook函数,内部可以使用 其他hook
        let [count, setCount] = useState(100)
    }

    // if(true){
    //     let [msg, setMsg] = useState('atguigu')
    // }

    for(let i=0; i<100;i++){
        let [msg, setMsg] = useState('atguigu')
    }
    return (
        <div>
            <button onClick={useSave}>点击</button>
        </div>
    )
}

这段代码要分三层看:useRef 在组件顶层,合法;save 是普通函数,内部调用 useState 会报错;useSave 是自定义 Hook,定义本身符合“以 use 开头”的约定。但 for 循环里的 useState 和 onClick={useSave} 都是易错点,实际项目里应把 Hook 调用移到组件顶层,事件回调里只做普通逻辑。转写稿里讲 useEffect 时强调“组件挂载完成后订阅、卸载前取消订阅”,本质也是要求 Hook 调用位置稳定;转写稿中“医保”是同音误写,应为“闭包”,本节先不展开。

一、自定义 Hook 的定位:复用逻辑,不是复用 UI ​

为什么需要:FuMing 和 Lili 都需要“鼠标移动时拿到坐标”的逻辑。如果每个组件各写一遍 useState + useEffect + addEventListener,代码重复,改一处要改多处。转写稿里反复强调 props 逐级传递的弊端,本质也是“相同数据/逻辑不要到处转交”;自定义 Hook 解决的是另一类重复:把组件内的状态逻辑抽出来复用。

怎么想:React 组件复用 UI,Hook 复用“带状态逻辑”。只要函数名以 use 开头,内部可以调用 useState、useEffect,就可以把坐标状态和鼠标监听封装起来。

怎么做:新建 hook/usePosition.js。注意笔记标题里的 usePostion 是拼写笔误,代码中实际是 usePosition,导入路径也要一致。

易错点:Hook 必须在组件或另一个 Hook 的顶层调用,不能放在 if、循环或普通回调里;函数名必须以 use 开头,否则 React 无法识别。

二、拆解 usePosition:状态、副作用与清理 ​

js
import { useEffect, useState } from "react";

function usePosition() {
    let [x, setX] = useState(110)
    let [y, setY] = useState(110)
    useEffect(() => {// componentDidMount
        function move(e) {
            console.log(e.clientX, e.clientY);
            setX(e.clientX)
            setY(e.clientY)
        }
        window.addEventListener('mousemove', move)
        return () => {// componentWillUnmount
            window.removeEventListener('mousemove', move)
        }
    }, [])
    return {
        x,y
    }
}

export default usePosition;

为什么需要:鼠标坐标来自浏览器事件,不是组件内部纯计算,所以要用 useEffect 建立外部事件监听;坐标会变化,所以要用 useState 保存。

怎么想:useEffect 的空依赖 [] 表示只在挂载时执行一次绑定;返回的函数在卸载时执行,用来移除监听。转写稿在 pubsub 部分强调“组件挂载时订阅、卸载时取消订阅”,这里的“绑定/移除 mousemove”是同一思路。

怎么做:在 move 中读取 e.clientX/e.clientY 并分别 setX/setY;把 move 定义在 effect 内部,保证绑定和移除使用同一个函数引用。

易错点:不要漏掉清理函数,否则组件卸载后监听仍在,反复挂载会重复绑定;依赖数组不要随意写 [x,y],否则每次坐标变化都会重新绑定监听;调试时先看控制台打印的坐标是否随鼠标变化,再检查页面定位。

三、FuMing:消费 Hook 返回值 ​

jsx
import React, { useState } from 'react'
import { useEffect } from 'react'
import usePosition from '../hook/usePosition'

export default function FuMing() {
    let {x,y} = usePosition()
    return (
        <div style={{position:'absolute',left:x,top:y,width:100,height:100,border:'2px solid green'}}>
            FuMing
        </div>
    )
}

为什么需要:组件只关心“当前坐标是多少”,不关心监听怎么绑定、状态怎么更新,所以调用 Hook 后解构 {x,y} 即可。

怎么想:usePosition 返回对象,组件用解构拿到坐标;left:x, top:y 直接驱动绝对定位。

怎么做:在组件顶层调用 usePosition(),把返回值用于 style。这里多导入的 useState/useEffect 不影响运行,但可删掉保持整洁。

易错点:不要在事件回调里调用 Hook;不要直接修改 Hook 返回的 x/y 来“改状态”,它们只是本次渲染的值。

四、Lili:同一 Hook 的第二次复用与偏移 ​

jsx
import React, { useEffect, useState } from 'react'
import usePosition from '../hook/usePosition'
export default function Lili() {
    let {x,y} = usePosition();
    x -= 110
    y -= 110
    return (
        <div style={{ position: 'absolute', left: x, top: y, width: 100, height: 100, border: '2px solid red' }}>Lili</div>
    )
}

为什么需要:Lili 也要跟随鼠标,但它的显示位置相对鼠标有偏移,所以复用坐标逻辑后,在组件内做业务偏移。

怎么想:Hook 负责“取坐标”,组件负责“怎么展示”。x -= 110 是对本次渲染的局部变量重新赋值,不会反向修改 Hook 内部状态。

怎么做:调用 usePosition() 后用 let 接收,再计算偏移并渲染。两个组件各自调用 Hook,会各自维护独立的 x/y 状态。

易错点:如果用 const {x,y} 再 x -= 110 会报错;偏移逻辑应放在渲染计算处,不要试图在 Hook 外改 Hook 状态;确认两个组件都导入同一个 usePosition 路径。

五、调试与规则检查 ​

为什么需要:自定义 Hook 出错时,表面看是“坐标不动”,实际可能是监听没绑上、清理没执行、Hook 调用位置不对。

怎么想:按“事件是否触发 → 状态是否更新 → 返回值是否被消费 → 组件是否重复挂载”排查。

怎么做:保留 console.log(e.clientX, e.clientY) 看事件;移动鼠标观察两个框是否都动;切换页面/卸载组件后确认没有残留监听;检查两个组件是否都从 ../hook/usePosition 导入。

易错点:Hook 只能在顶层调用;effect 清理函数要移除同一个 move;不要把 usePosition 写成普通函数却在条件分支里调用。按这些点检查,FuMing 与 Lili 就能共享同一套鼠标坐标逻辑,而各自保留自己的展示偏移。

作业目标:把类组件版 TodoList 改成函数组件版 ​

为什么需要: 类组件里的 state、生命周期和 this 写法,在函数组件中要换成 useState、useEffect 等 Hook。本节作业就是用函数组件重写 TodoList,并借 pubsub-js 完成跨组件通信。
怎么想: 先保留 TodoList 的数据源和列表渲染,再把“添加”“删除”从逐级 props 传递改成“谁发布消息、谁订阅消息”。转写稿里明确:Header 负责发布添加消息,TodoList 订阅后更新数据;Item 发布删除消息,TodoList 订阅后按 id 过滤。
怎么做: 组件拆成 TodoList、Header、Item。TodoList 用 useState 管理 todos,用 useEffect 在挂载后订阅消息,并在卸载时取消订阅。
易错点: 消息名必须前后一致;订阅产生的 id 要保存,卸载时用它取消订阅;列表 key 用 todo.id,不要用索引。

用 useState 管理 todos 并渲染列表 ​

为什么需要: 函数组件没有 this.state,数据变化必须通过 useState 触发重新渲染。
怎么想: 初始数据放在 TodoList 里,结构包含 id、title、isDone。转写稿中初始值类似 { id: nanoid(), title: '吃饭', isDone: false }。
怎么做:

jsx
import { useState } from 'react'
import { nanoid } from 'nanoid'
import Header from './Header'
import Item from './Item'

export default function TodoList() {
  const [todos, setTodos] = useState([
    { id: nanoid(), title: '吃饭', isDone: false }
  ])

  return (
    <div>
      <Header />
      <ul>
        {todos.map(todo => (
          <Item key={todo.id} todo={todo} />
        ))}
      </ul>
    </div>
  )
}

易错点: nanoid 要正确导入;map 必须有返回值;Item 接收的是整个 todo,不是只传标题。

用 pubsub-js 实现 Header 添加待办 ​

为什么需要: 如果 Header 和 TodoList 不是直接父子关系,用 props 要逐级传递,比较绕。pubsub-js 可以实现任意组件通信。
怎么想: 谁产生数据谁发布:Header 在回车时发布 ADD_TODO,数据是输入框的 title;谁维护数据谁订阅:TodoList 订阅 ADD_TODO,收到 title 后追加一条 todo。
怎么做:

jsx
// Header.jsx
import PubSub from 'pubsub-js'

export default function Header() {
  function handleKeyUp(e) {
    if (e.key !== 'Enter') return
    const title = e.target.value.trim()
    if (!title) return
    PubSub.publish('ADD_TODO', title)
    e.target.value = ''
  }

  return <input placeholder="请输入待办" onKeyUp={handleKeyUp} />
}
jsx
// TodoList.jsx 中补充订阅
import { useEffect } from 'react'
import PubSub from 'pubsub-js'

useEffect(() => {
  const addId = PubSub.subscribe('ADD_TODO', (msg, title) => {
    const newTodo = { id: nanoid(), title, isDone: false }
    setTodos(prev => [...prev, newTodo])
  })

  return () => {
    PubSub.unsubscribe(addId)
  }
}, [])

易错点: 不要在 useEffect 里直接写 setTodos([...todos, newTodo])。转写稿调试时发现“永远只能添加一条”,原因就是 useEffect 回调形成闭包,拿到的 todos 永远是初始值。必须用 setTodos(prev => ...) 的函数式更新。

用 pubsub-js 实现 Item 删除待办 ​

为什么需要: 删除按钮在 Item 里,但真正要修改的是 TodoList 的 todos,同样适合用发布订阅。
怎么想: Item 点击删除时发布 DELETE_TODO,把 todo.id 作为数据传出去;TodoList 订阅后按 id 过滤。
怎么做:

jsx
// Item.jsx
import PubSub from 'pubsub-js'

export default function Item({ todo }) {
  function handleDelete() {
    PubSub.publish('DELETE_TODO', todo.id)
  }

  return (
    <li>
      <span>{todo.title}</span>
      <button onClick={handleDelete}>删除</button>
    </li>
  )
}
jsx
// TodoList.jsx 中再订阅删除消息
useEffect(() => {
  const deleteId = PubSub.subscribe('DELETE_TODO', (msg, id) => {
    setTodos(prev => prev.filter(todo => todo.id !== id))
  })

  return () => {
    PubSub.unsubscribe(deleteId)
  }
}, [])

易错点: 删除也不能直接写 setTodos(todos.filter(...)),否则同样会受闭包影响,只能拿到初始 todos。转写稿里删除第二个待办时验证:按 id 过滤后只删对应项,不能把列表整体清空。

调试过程与检查清单 ​

为什么需要: 发布订阅是“先订阅、后发布”,顺序和取消订阅容易漏。
怎么想: 先刷新页面让 TodoList 完成订阅,再在 Header 回车发布;点击删除时确认消息名、id 参数、过滤条件一致。
怎么做: 检查 PubSub.subscribe 的返回值是否保存;useEffect 返回函数里是否 unsubscribe;添加用 [...prev, newTodo],删除用 prev.filter(...)。
易错点: 消息名写成 ADD_TODO 和 add_todo 不一致会收不到;忘记 return 取消订阅会造成重复订阅;输入框发布后要清空;其余复选框等功能可按同样“Item 发布、TodoList 订阅”的思路完成。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一:hook 函数的使用原则(自定义 hook 与三种违规写法)

jsx
// src/components/FunTest.jsx
import React, { useRef, useState } from 'react'

export default function FunTest() {
  const usernameRef = useRef()
  const [count, setCount] = useState(0)

  // ❌ 错误1:在普通函数里调用 hook
  // 报错:React Hook "useState" is called in function "save" that is
  // neither a React function component nor a custom React Hook function.
  // function save() {
  //   let [count, setCount] = useState(100)
  // }

  // ✅ 正确:函数名以 use 开头 + 首字母大写,React 才认它是自定义 hook
  function useSave() {
    let [num, setNum] = useState(100)
    return { num, setNum }
  }

  // ❌ 错误2:写在 if 语句中
  // if (true) {
  //   let [msg, setMsg] = useState('atguigu')
  // }

  // ❌ 错误3:写在 for 循环中(hook 数量必须是确定的)
  // for (let i = 0; i < 100; i++) {
  //   let [msg, setMsg] = useState('atguigu')
  // }

  // ✅ 正确:自定义 hook 只能在组件顶层或其它 hook 里调用
  const { num, setNum } = useSave()

  return (
    <div>
      <input ref={usernameRef} placeholder="用户名" />
      <p>count: {count}</p>
      <button onClick={() => setCount(count + 1)}>count + 1</button>
      <p>useSave.num: {num}</p>
      <button onClick={() => setNum(num + 1)}>num + 1</button>
    </div>
  )
}

要点:

  • useSave 之所以能被识别成自定义 hook,唯一原因就是函数名 use + 首字母大写;叫 save、fn 都会被当成普通函数,内部调 useState 直接报错。
  • hook 只能在函数组件和自定义 hook 里调用,类组件里用不了。
  • if / for 里调 hook 会报错:React 靠调用顺序来对应每个 hook 的状态,条件或循环会让顺序错乱,所以 hook 数量必须确定。
  • 上面的 if 和 for 只注释掉不删除,是为了对照笔记里的报错现象;实际项目里绝对不能保留。

示例二:自定义 hook —— usePosition(封装鼠标跟随逻辑)

js
// src/hook/usePosition.js
import { useEffect, useState } from 'react'

function usePosition() {
  let [x, setX] = useState(110)
  let [y, setY] = useState(110)

  useEffect(() => {                      // 相当于 componentDidMount
    function move(e) {
      setX(e.clientX)
      setY(e.clientY)
    }
    window.addEventListener('mousemove', move)

    return () => {                       // 相当于 componentWillUnmount
      window.removeEventListener('mousemove', move)
    }
  }, [])                                 // 空依赖:只挂载一次

  return { x, y }
}

export default usePosition

要点:

  • 自定义 hook 的本质就是把组件里重复的逻辑抽出来复用,它本身不是组件,没有 JSX,只返回数据/方法。
  • 函数名必须是 usePosition(use 开头 + 首字母大写),否则内部调用 useState/useEffect 会报错。
  • useEffect 第二个参数传 [],才能保证只注册一次监听;漏了 [] 会每次渲染都重复绑定。
  • return 里必须写 removeEventListener 做清理,否则组件卸载后监听还挂着,会内存泄漏(还会因 setState 报警告)。
  • move 函数定义在 useEffect 内部,是为了让 return 里的清理函数能拿到同一个函数引用;定义在外面就解绑不掉了。

示例三:多个组件复用同一个自定义 hook(FuMing + Lili)

jsx
// src/components/FuMing.jsx
import React from 'react'
import usePosition from '../hook/usePosition'

export default function FuMing() {
  let { x, y } = usePosition()
  return (
    <div
      style={{
        position: 'absolute',
        left: x,
        top: y,
        width: 100,
        height: 100,
        border: '2px solid green',
      }}
    >
      FuMing
    </div>
  )
}
jsx
// src/components/Lili.jsx
import React from 'react'
import usePosition from '../hook/usePosition'

export default function Lili() {
  let { x, y } = usePosition()
  x -= 110
  y -= 110
  return (
    <div
      style={{
        position: 'absolute',
        left: x,
        top: y,
        width: 100,
        height: 100,
        border: '2px solid red',
      }}
    >
      Lili
    </div>
  )
}

要点:

  • 两个组件各自调用 usePosition(),得到的是各自独立的一份状态,互不干扰(hook 复用逻辑,不复用状态)。
  • 两个组件都注册了 mousemove 监听,所以都能跟着鼠标走;这正是自定义 hook 的价值——写一次逻辑,多处复用。
  • Lili 里的 x -= 110 只是改了本次渲染用到的局部变量,不会改到 hook 内部的 state,页面再次渲染时又会从 hook 的原始值算起;如果想让偏移持久生效,应该在 usePosition 内部处理或另传参数。
  • 所以这里必须用 let 解构,用 const 会直接报“常量不可重新赋值”的错。
  • 使用前记得在父组件里渲染 <FuMing /> 和 <Lili />,并确保两个文件路径 ../hook/usePosition 正确,否则会报模块找不到。

常见错误 ​

  • 现象:useState 写进 if、for 或普通函数 save 中,报 “React Hook ... is called in ...”。原因:Hook 调用位置/顺序不符合规则。修法:只在函数组件或 useXxx 自定义 Hook 的顶层调用,条件/循环逻辑放到 Hook 内部或拆组件。
  • 现象:自定义 Hook 不以 use 开头,或组件调用后拿不到状态。原因:命名与调用约定混乱,把普通函数当 Hook。修法:统一命名为 useXxx,只在函数组件/其他 Hook 顶层调用,并明确返回值。
  • 现象:useEffect 中 addEventListener 后切换页面仍触发、报内存泄漏。原因:未清理副作用。修法:在 useEffect 中 return () => removeEventListener(...)。
  • 现象:PubSub 订阅回调里 setTodos([...todos, newTodo]) 永远只加一条,删除也基于旧列表。原因:useEffect 闭包捕获初始 todos。修法:改用 setTodos(prev => ...),订阅与取消订阅放在同一 useEffect。
  • 现象:useContext 读不到 Provider 的 value。原因:创建、Provider、useContext 使用的不是同一个 context 对象,或后代未被包裹。修法:统一从同一模块导入 context,确认 Provider 包裹目标后代。
  • 现象:axios 拦截器配置后请求发不出,或响应拿不到 data。原因:请求拦截器忘记 return config,响应拦截器忘记 return response/response.data。修法:请求拦截器必须 return config;响应拦截器明确返回简化后的数据,并统一调用处约定。
  • 现象:axios 的 get/delete/post/put 参数位置写错,query 与 body 混淆。原因:对象式用法分两类。修法:get/delete(url, config);post/put/patch(url, data, config)。
  • 现象:401 未跳登录,或错误继续进入业务 then。原因:响应拦截器未清 token/重定向,错误分支未阻断 Promise 链。修法:错误分支清 token、跳登录;需要阻断时 return new Promise(() => {})。

练习 ​

  1. 改错题:给出包含 useState 在 if、for、普通函数中的代码,指出所有违规点并改成合法写法。
  2. 封装 usePosition,在 FuMing、Lili 两个组件中复用,要求补全鼠标监听清理逻辑。
  3. 用 PubSub 改写 TodoList 的添加/删除:写出订阅、取消订阅、发布位置,并解决回调中拿到旧 todos 的问题。
  4. 用 useContext 实现 App → Son 跨级传递 username/age,要求 Father 不用 props 中转,写出关键步骤。
  5. axios 基础配置:配置 baseURL/timeout,请求拦截器带 token 和 NProgress.start(),响应拦截器关闭 loading、简化响应数据、401 清 token 跳登录、统一错误处理;标出所有必须 return 的位置。

原视频对照 ​

#视频时长
1props通信总结06:09
2useContext实现祖先组件向任意后代组件通信14:53
3pubsub实现任意组件通信09:51
4pubsub基本使用14:45
5pubsub改写todolist12:15
6axios基本配置使用15:30
7axios请求基本用法06:53

小结 ​

  • Hook 的硬约束是调用位置:函数组件/自定义 Hook 顶层,不能类组件、普通函数、条件或循环中调用。
  • 自定义 Hook 复用逻辑而非 UI;返回值按调用方设计,副作用必须清理。
  • props 适合父子及兄弟经父传递;跨级共享用 Context,前提是同一个 context 对象且 Provider 包裹后代。
  • PubSub 适合任意组件通信;订阅在挂载、取消在卸载,回调更新状态优先函数式更新避免闭包旧值。
  • axios 基础配置统一 baseURL/timeout;拦截器必须 return config/response,否则链路中断。
  • 响应拦截器集中处理 loading 关闭、数据简化、401 重定向和错误兜底;错误阻断要显式处理。
  • TodoList 重写重点是状态归属、通信方式选择、闭包与清理,而不是堆 API。