Skip to content

localStorage与类组件小结(Day6 3/3) ​

本页对应 React + TypeScript + 医通项目,共 5 段视频,约 43 分钟。

学习目标 ​

  • 能分别使用 setMsg(新值) 和 setMsg(回调函数) 更新状态,并说明两种写法在取值来源上的差异。
  • 能识别定时器或事件回调中因闭包导致 setMsg 只能读取初始值的问题,并改用回调函数写法获取最新状态完成修复。
  • 能判断一段 Hook 调用是否合法:不在类组件、普通函数、条件语句或循环中调用,只在函数组件或其他自定义 Hook 中调用。
  • 能根据函数命名规则区分普通函数、函数组件和自定义 Hook,并说明 React 对 use 开头且后接大写字母的识别规则。
  • 能使用 useState、useEffect 和自定义事件实现鼠标坐标跟随示例,并在组件卸载时正确移除事件监听。
  • 能将重复的鼠标坐标逻辑抽取为 usePosition 自定义 Hook,在多个组件中解构复用返回值并验证功能一致。

知识点 ​

本节说明与 API 速查 ​

本节转写稿实际讨论的是 React 状态、闭包与 Hook 使用原则,和“14-4. localStorage”这节笔记不是同一主题。为避免把无关内容硬套进来,下面严格依据本节笔记整理 localStorage;转写稿中的 Hook 推导不混入本节,避免编造。

本节笔记核心如下:

本地存储

  1. 存储:localStorage.setItem(key,value)
  2. 读数据:localStorage.getItem(key)
  3. 删除:localStorage.removeItem(key)
  4. 全部删除:localStorage.clear()
    注意:本地存储,存储的数据只能存基本数据类型,存不了对象和数组。
    想存对象和数组怎么办?
    JSON.stringify 将对象或数组转化为 json 格式的字符串。
    读取数据:JSON.parse(JSON 格式字符串) ===》 JSON 格式字符串还原成对象或数组。

存储:localStorage.setItem(key, value) ​

为什么需要:页面刷新后,普通 JavaScript 变量会重新初始化;如果希望某些数据在浏览器关闭再打开后仍然存在,就需要本地存储。
怎么想:localStorage 是“键值对”存储,给一个 key,再给一个 value,就能把数据写进当前源下的本地存储空间。
怎么做:

js
localStorage.setItem('username', 'at硅谷');
localStorage.setItem('age', 18); // 实际会以字符串 "18" 保存

易错点:localStorage 的 key 和 value 最终都会按字符串处理;同名 key 会覆盖旧值;它受同源策略限制,不同源页面不能直接共享。

读取:localStorage.getItem(key) ​

为什么需要:存进去的数据要在后续页面或刷新后取出来使用。
怎么想:用同一个 key 读取;如果这个 key 不存在,getItem 返回 null。
怎么做:

js
const username = localStorage.getItem('username');
const age = localStorage.getItem('age'); // "18",不是数字 18
const missing = localStorage.getItem('not-exist'); // null

易错点:读出来的值类型是字符串或 null,不要假设它还是数字、布尔值。需要数字时用 Number(age) 转换;使用前先判断是否为 null。

删除:localStorage.removeItem(key) ​

为什么需要:用户退出登录、清除某个临时配置时,只应删除指定数据,而不是全部清空。
怎么想:明确要删除的 key,调用 removeItem。
怎么做:

js
localStorage.removeItem('username');

易错点:删除不存在的 key 不会报错,但不要因此误以为删除成功;key 名要前后一致。

全部删除:localStorage.clear() ​

为什么需要:调试、重置应用状态或用户主动“清除全部本地数据”时,需要一次性清空当前源下的 localStorage。
怎么想:clear() 清空的是当前源下的全部 localStorage,不是只清某一个 key。
怎么做:

js
localStorage.clear();

易错点:不要在不该清空的地方随便调用,否则会把同源下其他业务数据一起删掉;它不会删除 sessionStorage 或 Cookie。

只能存基本数据类型:对象和数组不能直接存 ​

为什么需要:实际开发中经常要存用户信息、购物车、配置对象,这些往往是对象或数组。
怎么想:笔记明确提醒:本地存储只能存基本数据类型,存不了对象和数组。直接存对象,会被转成无意义的字符串。
怎么做:不要这样写:

js
localStorage.setItem('user', { name: 'at硅谷' });
// 实际会变成 "[object Object]",信息丢失

易错点:看到 [object Object] 就说明对象被错误地直接存了;数组直接存也会变成逗号拼接的字符串,结构不可靠。

JSON.stringify 与 JSON.parse 保存对象和数组 ​

为什么需要:对象和数组不能直接存,但可以先把它们序列化成 JSON 格式字符串,再存字符串;读取时再反序列化还原。
怎么想:存之前 JSON.stringify,取之后 JSON.parse,一进一出保持结构。
怎么做:

js
const user = { name: 'at硅谷', age: 18 };
localStorage.setItem('user', JSON.stringify(user));

const raw = localStorage.getItem('user');
const userObj = raw ? JSON.parse(raw) : null;
console.log(userObj.name); // at硅谷

数组同理:

js
const list = [1, 2, 3];
localStorage.setItem('list', JSON.stringify(list));

const listStr = localStorage.getItem('list');
const listArr = listStr ? JSON.parse(listStr) : [];

易错点:JSON.parse 只能解析合法 JSON 字符串,解析失败会抛错,建议用 try...catch;JSON.stringify 会丢失函数、undefined 等特殊值,循环引用对象也会报错。调试时可在浏览器开发者工具 Application → Local Storage 中查看,看到的 value 都是字符串。

类组件的定义 ​

为什么需要:当页面逻辑既要管理内部状态,又要在挂载、更新、卸载时做副作用处理时,类组件能把状态、生命周期和渲染逻辑组织在同一个类里。怎么想:先把它看成一个“会渲染 UI 的类”,内部通过 this 访问状态和方法,渲染入口是 render。怎么做:定义类并继承 React.Component,在 render 中返回 JSX;需要外部数据时从 this.props 取,需要内部数据时从 this.state 取。易错点:类组件里不能使用 Hook。转写稿中在类组件里调用 useRef,直接报 Invalid hook call,提示 Hook 只能在函数组件或自定义 Hook 中调用;这个报错要会看,不要误以为是导入问题。

状态数据 state ​

为什么需要:组件内部有些数据会随用户操作变化,例如输入内容、计数、开关,这类数据适合放在 state。怎么想:state 是类实例上的对象,读取用 this.state.xxx,修改不能直接赋值,而要走 setState,让组件重新渲染。怎么做:在构造函数或类字段中定义初始状态;渲染时读取;事件回调里调用 this.setState 更新。易错点:不要写 this.state.msg = ... 直接改。转写稿用函数组件的 setMsg 演示了一个典型调试过程:定时器 2 秒后执行 setMsg,先点按钮加叹号,2 秒后却变成 atguigu-。推导原因是定时器回调形成闭包(转写稿里的“B包”),捕获的是挂载时执行栈(“执行站”)里的 MSG 初始值,不是缓存区(“换存区”)最新值;改用回调函数形参(“行参”)取最新值即可,如 setMsg(msg => msg + '-')。排查状态不对时,先看回调闭包是否拿到旧值。

类组件的外部数据 props ​

为什么需要:组件之间要通信,父组件把数据或行为传给子组件,子组件把用户操作结果传回父组件。怎么想:父传子用属性;子传父用父组件传下去的回调函数,子组件调用回调并传参;props 是外部数据,只读。怎么做:父组件写 <Child name={name} onSave={fn} />,子组件用 this.props.name 读取、用 this.props.onSave(value) 回传;需要插槽内容时用 this.props.children;需要约束时加类型、必填、默认值限定。易错点:不要直接修改 props;子传父时回调要记得传参;children 不是普通属性,要按插槽内容理解。

类组件生命周期 ​

为什么需要:挂载后要开定时器、发请求、订阅消息、绑自定义事件;更新后要发请求或更新本地存储;卸载前要清理,否则会泄漏。怎么想:挂载阶段做“初始化副作用”,更新阶段做“依赖变化后的副作用”,卸载阶段做“清理”。怎么做:componentDidMount 开定时器、发 ajax、订阅消息、绑自定义事件;更新由 new props、setState、forceUpdate 触发,在 componentDidUpdate 发 ajax、更新本地存储;componentWillUnmount 关定时器、取消订阅、解绑自定义事件。转写稿用 useEffect 模拟挂载/卸载:挂载时 window.addEventListener('mousemove', move),卸载时 window.removeEventListener('mousemove', move),组件就能跟随鼠标;调试“事件没生效”先查事件名,调试“没解绑”先查是否同一个函数引用。易错点:解绑和绑定必须用同一个函数;定时器、订阅、事件都要在卸载阶段清理。

js
window.addEventListener('mousemove', move)
window.removeEventListener('mousemove', move)

ref、受控组件与非受控组件 ​

为什么需要:有时要获取真实 DOM 或表单输入值;表单又分“交给状态管”和“交给 DOM 管”两种写法。怎么想:受控组件把 value、checked 交给状态,用户输入通过 onChange 改状态;非受控组件用 defaultValue、defaultChecked 给初始值,需要取值时用 ref 获取真实 DOM。怎么做:受控写 value={this.state.x} 并补 onChange;非受控写 defaultValue,再用 ref.current.value 读取。易错点:受控组件只写 value 不写 onChange 会变成只读;非受控不要再用 value 强控。

组件化开发思路 ​

为什么需要:页面复杂后要拆分、复用,数据流才清晰。怎么想:先静态布局拆组件,再定首屏数据结构和数据定义在谁身上,最后按交互拆步骤。怎么做:静态布局按区域拆;首屏分析数据结构、分析数据归属;交互按“干了什么→什么结果→拆步骤→匹配技术点”落地。转写稿把两个组件重复的鼠标跟随逻辑抽到 usePosition,返回 X、Y,两个组件直接复用,说明组件间相同逻辑可以封装成自定义 Hook。易错点:Hook 名必须以 use 开头,React 才认;不能在类组件或普通函数中调用 Hook;Hook 数量要确定,不能放 if、for 里。

从函数组件的缺陷说起:Hook 解决了什么 ​

为什么需要:在 React 16.4 以前,函数组件没有状态,也没有生命周期,只能做展示;类组件才有 state、生命周期和 ref。这让“想用函数写法”和“需要组件能力”之间产生矛盾。

怎么想:16.8 推出 Hook 后,函数组件通过 Hook 获得类组件的能力:状态用 useState,生命周期用 useEffect,ref 用 useRef。转写稿里的“us stage”应纠正为 useState,“U Effect”是 useEffect。

怎么做:在函数组件里直接调用这些 Hook,例如 const [msg, setMsg] = useState('@硅谷')。它们本质上是函数,但 React 对它们有特殊识别规则。

易错点:不要把 Hook 当普通函数随手调用;Hook 的调用位置和调用顺序都有约束,后面会展开。

useState 更新状态:为什么定时器里拿到了旧值 ​

为什么需要:状态更新看似简单,但函数组件每次渲染都会重新执行函数,useEffect 里的回调可能形成闭包,拿到某次渲染时的旧变量。

怎么想:转写稿中的现象是:点击按钮给 msg 加叹号,2 秒后定时器却把 msg 变成 @硅谷-,而不是 @硅谷!-。原因是 useEffect(..., []) 只在挂载时执行一次,其中的 setTimeout 回调闭包捕获的是首次渲染的 msg,即初始值 @硅谷。

怎么做:先还原关键代码:

jsx
function App() {
  const [msg, setMsg] = useState('@硅谷');

  useEffect(() => {
    setTimeout(() => {
      setMsg(msg + '-'); // 这里拿到的是挂载时的 msg
    }, 2000);
  }, []);

  return (
    <div>
      {msg}
      <button onClick={() => setMsg(msg + '!')}>加叹号</button>
    </div>
  );
}

当值依赖上一次状态时,改用函数式更新:

jsx
setMsg(prev => prev + '-');

这样回调参数 prev 取的是缓存区中最新的状态值,能在叹号后继续加减号。

易错点:不是所有 setState 都必须用函数式;值正确时第一种写法最简单。但凡发现“定时器、事件回调里值不对”,要立刻想到闭包,并考虑 setMsg(prev => ...)。

Hook 使用原则:名字、位置、数量 ​

为什么需要:React 依靠 Hook 的调用顺序来匹配内部状态。若在类组件、普通函数、条件或循环里随意调用,顺序会乱,React 无法正确对应。

怎么想:React 识别 Hook 的规则很“表面”:函数名以 use 开头,且 use 后首字母大写。useState、useEffect 是 Hook;usePosition 也会被当作自定义 Hook;use position 或 fn1 不是。组件则通常看函数名首字母大写。

怎么做:遵守三条原则:不能在类组件中使用;不能在普通函数中使用,只能写在函数组件或其他 Hook 中;Hook 数量必须确定,不能写在 if、for 等可能改变调用次数的语句里。

jsx
class Test extends React.Component {
  userNameRef = useRef(); // 报错:Invalid hook call
}

function save() {
  const [count, setCount] = useState(100); // 报错:普通函数中调用 Hook
}

function useSave() {
  const [count, setCount] = useState(100); // 可以:自定义 Hook
}

条件调用也会报错:

jsx
if (true) {
  const [msg, setMsg] = useState('@硅谷'); // 报错:Hook 被有条件地调用
}

易错点:自定义 Hook 名字必须 use 开头且后首字母大写;不要用条件 return 或循环“绕过”规则;类组件里不能混用 Hook。

自定义 Hook:把重复逻辑抽出来 ​

为什么需要:转写稿中“复明”和“丽丽”两个组件都要监听鼠标移动并更新坐标,重复代码很多。组件间相同逻辑应封装复用。

怎么想:把“监听鼠标坐标”的逻辑放进一个自定义 Hook,返回 {x, y};组件只负责使用坐标。自定义 Hook 内部可以调用 useState、useEffect,因为它本身就是 Hook。

怎么做:

jsx
// hooks/usePosition.js
import { useState, useEffect } from 'react';

export default function usePosition() {
  const [x, setX] = useState(110);
  const [y, setY] = useState(110);

  useEffect(() => {
    function move(e) {
      setX(e.clientX);
      setY(e.clientY);
    }
    window.addEventListener('mousemove', move);
    return () => window.removeEventListener('mousemove', move);
  }, []);

  return { x, y };
}

组件中使用:

jsx
const { x, y } = usePosition();

<div style={{ position: 'absolute', left: x, top: y }}>
  复明
</div>

丽丽组件复用同一逻辑,并做偏移:

jsx
const { x, y } = usePosition();

<div style={{ position: 'absolute', left: x - 110, top: y - 110 }}>
  丽丽
</div>

易错点:添加事件后要在 useEffect 返回的清理函数里移除,且事件名要一致;自定义 Hook 返回对象后组件要按需解构;两个组件坐标相同会重叠,偏移量要按实际效果调整。这样封装后,重复代码明显减少,逻辑也更清晰。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

代码示例 ​

示例 1 · localStorage:基本类型与对象/数组的存取

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>localStorage 本地存储</title>
</head>
<body>
  <button id="save">存储对象</button>
  <button id="read">读取对象</button>
  <button id="del">删除 username</button>
  <button id="clear">全部清空</button>
  <pre id="log"></pre>

  <script>
    const log = (v) => {
      document.getElementById('log').textContent += v + '\n';
    };

    // 1. 存基本数据类型
    localStorage.setItem('username', 'atguigu');
    log('读取 username:' + localStorage.getItem('username')); // atguigu

    // 2. 存对象 / 数组:必须先用 JSON.stringify 转成 JSON 格式字符串
    const user = { name: 'atguigu', age: 10, hobby: ['vue', 'react'] };
    localStorage.setItem('user', JSON.stringify(user));

    // 3. 读数据:拿到的是 JSON 格式字符串,必须 JSON.parse 还原成对象
    const raw = localStorage.getItem('user');
    log('原始值(字符串):' + raw);
    const obj = JSON.parse(raw);
    log('还原后取 name:' + obj.name);

    // 4. 删除单个 / 全部删除
    document.getElementById('save').onclick = () =>
      localStorage.setItem('user', JSON.stringify(user));

    document.getElementById('read').onclick = () => {
      const str = localStorage.getItem('user');
      log('user = ' + JSON.stringify(str ? JSON.parse(str) : null));
    };

    document.getElementById('del').onclick = () => {
      localStorage.removeItem('username');
      log('username 已删除,getItem 返回:' + localStorage.getItem('username'));
    };

    document.getElementById('clear').onclick = () => {
      localStorage.clear();
      log('已全部清空');
    };
  </script>
</body>
</html>

关键行 & 易错点

  • setItem(key, value) 存、getItem(key) 读、removeItem(key) 删单个、clear() 全部删除,四个 API 一一对应笔记里的四条。
  • 本地存储只能存基本数据类型(本质是字符串):直接 localStorage.setItem('user', user) 会得到 "[object Object]",取出来再 JSON.parse 直接抛错。
  • 存对象/数组 → JSON.stringify;读出来 → JSON.parse,两步必须成对出现,漏掉任何一步都会拿到 undefined。
  • getItem 取不存在的 key 返回的是 null,不是 undefined;JSON.parse(null) 得到 null,但 JSON.parse(undefined) 会抛 SyntaxError,所以解析前要先判空。
  • localStorage 按域名隔离、没有过期时间(区别于 sessionStorage 关闭标签页即清空)。

示例 2 · useState 的闭包坑与 setState 回调函数写法

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>setMsg 用法</title>
  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script 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;

    function App() {
      const [msg, setMsg] = useState('at硅谷');

      // 模拟 componentDidMount:挂载完成后开启定时器
      useEffect(() => {
        const timer = setTimeout(() => {
          // ❌ 第一种写法:只能取到挂载时那个作用域里的初始值
          // setMsg(msg + '-');

          // ✅ 第二种写法:回调函数的形参是缓存区里最新变化后的 msg
          setMsg((prev) => prev + '-');
        }, 2000);

        // 模拟 componentWillUnmount:卸载时关闭定时器
        return () => clearTimeout(timer);
      }, []);

      return (
        <div>
          <h1>{msg}</h1>
          <button onClick={() => setMsg(msg + '!')}>MSG 连上叹号</button>
        </div>
      );
    }

    ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  </script>
</body>
</html>

关键行 & 易错点

  • useState 返回 [状态, 修改状态的方法];状态真正的值存在 hook 自己的缓存空间里,组件作用域里的 msg 只是每次渲染时的快照。
  • useEffect(回调, []) 的 [] 就是"只在挂载后执行一次",等价于 componentDidMount;return 的函数等价于 componentWillUnmount。
  • 闭包坑:定时器回调是在首次渲染的作用域里形成的,setMsg(msg + '-') 里的 msg 永远是最初的 'at硅谷'。所以不管点多少次叹号,2 秒后都会被覆盖成 at硅谷-。
  • 正确姿势是 setMsg(prev => prev + '-'):形参 prev 取自 hook 缓存区里的最新值,返回值才是要写回缓存区的新值。
  • 易错点:看到"状态改了又被还原/只按初始值算",第一反应就是闭包,立刻换成函数式写法;同时定时器一定要 clearTimeout,否则卸载后还会 setState。

示例 3 · 自定义 hook:把组件间重复逻辑抽成 usePosition

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>自定义 hook - usePosition</title>
  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
  <style>
    .box { position: absolute; width: 100px; height: 100px; border: 2px solid #333; }
    .fuming { background: green; }
    .lili   { background: red; }
  </style>
</head>
<body>
  <div id="root"></div>

  <script type="text/babel">
    const { useState, useEffect } = React;

    // ---------- 自定义 hook:名字必须 use 开头 + 首字母大写 ----------
    function usePosition() {
      const [x, setX] = useState(110);
      const [y, setY] = useState(110);

      useEffect(() => {
        // 鼠标移动的自定义事件,组件挂载时绑定
        function move(e) {
          setX(e.clientX); // 鼠标距离屏幕左边的距离
          setY(e.clientY); // 鼠标距离屏幕上边的距离
        }
        window.addEventListener('mousemove', move);

        // 组件卸载时解绑,防止内存泄漏
        return () => window.removeEventListener('mousemove', move);
      }, []);

      return { x, y }; // 自定义 hook 可以有返回值,把逻辑结果交出去
    }

    // ---------- 复明:跟着鼠标走 ----------
    function Fuming() {
      const { x, y } = usePosition();
      return (
        <div className="box fuming" style={{ left: x, top: y }}>
          复明
        </div>
      );
    }

    // ---------- 丽丽:复用同一套逻辑,坐标减 110 永远追不上 ----------
    function Lili() {
      const { x, y } = usePosition();
      return (
        <div className="box lili" style={{ left: x - 110, top: y - 110 }}>
          丽丽
        </div>
      );
    }

    function App() {
      return (
        <div>
          <Lili />
          <Fuming />
        </div>
      );
    }

    ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  </script>
</body>
</html>

关键行 & 易错点

  • 自定义 hook 本质上就是一个普通函数,和普通函数唯一的区别是:函数名以 use 开头且后面首字母大写,React 才认它是 hook(usePosition 是,useposition 不是)。
  • 因为它名字合法,内部才可以调用 useState、useEffect 等其它 hook —— 这就是"hook 只能在函数组件或其它 hook 中调用,不能写在普通函数/类组件里"的原则。
  • addEventListener 与 removeEventListener 必须成对,且事件名、处理函数要一致;转写稿里把 move 提到组件外层是为了卸载时能拿到同一个引用,放在 useEffect 内部闭包同样成立,写法更内聚。
  • 状态不共享、逻辑共享:Fuming 和 Lili 各调用一次 usePosition,拿到的是各自独立的 x/y,只是把重复代码抽走了,所以两套坐标互不影响。
  • 易错点:hook 不能写在 if、for 里(数量必须确定);return 前面的 hook 一旦被条件跳过,就会报 "React Hook is called conditionally"。

常见错误 ​

  1. 现象:定时器 2 秒后执行 setMsg(msg + '-'),结果只保留初始值,之前点击追加的 ! 全丢失。
    原因:定时器回调形成闭包,捕获的是挂载时那次渲染中的 msg,不是最新状态。
    修法:依赖旧状态更新时改用函数式写法:setMsg(prev => prev + '-')。

  2. 现象:在类组件里调用 useRef / useState,直接报 Invalid hook call。
    原因:Hook 只能用于函数组件或自定义 Hook,不能用于类组件。
    修法:改为函数组件;或把逻辑抽成 useXxx 自定义 Hook 后在函数组件中调用。

  3. 现象:在普通事件回调函数里调用 useState,报“只能在函数组件或自定义 Hook 中调用”。
    原因:该函数既不是 React 函数组件,也不是自定义 Hook。
    修法:把状态声明移到组件顶层,普通回调里只调用 setState。

  4. 现象:把 useState 写在 if 或 for 中,运行报 Hook 数量/顺序相关错误。
    原因:条件或循环会导致 Hook 调用数量不确定,破坏 Hook 调用顺序。
    修法:Hook 一律放在组件顶层无条件调用;条件逻辑放到回调或渲染分支里。

  5. 现象:localStorage.setItem('list', obj) 后读到 [object Object],或 JSON.parse 报错。
    原因:localStorage 只能存字符串;对象/数组未序列化,读取时也未反序列化或未处理空值。
    修法:存用 JSON.stringify(obj),读用 JSON.parse(str),并对 null、非法 JSON 做兜底。

  6. 现象:组件卸载后定时器还在跑、鼠标事件仍触发,甚至出现重复绑定。
    原因:挂载时添加了定时器/事件订阅,卸载时没有清理。
    修法:useEffect 返回清理函数,或类组件在 componentWillUnmount 中 clearInterval / removeEventListener。

  7. 现象:自定义 Hook 不叫 useXxx,内部调用 Hook 报错或规则识别异常。
    原因:React 依靠 use 开头且后接大写的命名识别 Hook。
    修法:统一命名为 usePosition、useFetch 这类形式。

练习 ​

  1. 复现“定时器 2 秒后只保留初始值”的案例,分别用 setMsg(msg + '-') 和 setMsg(prev => prev + '-') 运行,记录两种输出差异并解释触发时机。

  2. 下面代码在类组件中调用了 useRef,请改造成可运行的函数组件,并说明为什么不能只把 useRef 换成普通变量。

    jsx
    class Test extends React.Component &#123;
      ref = useRef(null);
    &#125;
  3. 实现 saveCart(cart) 与 loadCart():把数组存入 localStorage,读取时处理空值、非法 JSON,返回空数组兜底。

  4. 编写 usePosition():返回 { x, y },组件挂载时监听 mousemove,卸载时解绑;再用两个组件复用该 Hook。

  5. 判断下列名称在 React 中更可能被当作“函数组件 / Hook / 普通函数”,并说明理由:usePosition、UsePosition、position、Position、use_position。

原视频对照 ​

#视频时长
1setMsg用法(上)12:44
2setMsg用法-回调函数(下)02:06
3hook函数使用原则16:15
4自定义hook应用-未封装前08:54
5自定义hook应用-封装后03:32

小结 ​

  1. 状态更新依赖旧值时,优先使用函数式 setState,避免闭包拿到旧值。
  2. Hook 的调用位置是硬约束:函数组件或自定义 Hook 的顶层,不能进类组件、普通函数、条件或循环。
  3. useXxx 命名不是风格问题,而是 React 识别 Hook 的规则。
  4. localStorage 存对象/数组必须序列化,读取必须反序列化并做异常兜底。
  5. 副作用要成对管理:挂载添加定时器/订阅/事件,卸载必须清理。
  6. 自定义 Hook 适合复用“逻辑”,不是复用 UI;返回值按调用方需要设计。
  7. 排查状态异常时,先查闭包旧值、Hook 调用顺序和清理函数三件事。