主题
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 推导不混入本节,避免编造。
本节笔记核心如下:
本地存储
- 存储:
localStorage.setItem(key,value)- 读数据:
localStorage.getItem(key)- 删除:
localStorage.removeItem(key)- 全部删除:
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 · 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"。
常见错误
现象:定时器 2 秒后执行
setMsg(msg + '-'),结果只保留初始值,之前点击追加的!全丢失。
原因:定时器回调形成闭包,捕获的是挂载时那次渲染中的msg,不是最新状态。
修法:依赖旧状态更新时改用函数式写法:setMsg(prev => prev + '-')。现象:在类组件里调用
useRef/useState,直接报Invalid hook call。
原因:Hook 只能用于函数组件或自定义 Hook,不能用于类组件。
修法:改为函数组件;或把逻辑抽成useXxx自定义 Hook 后在函数组件中调用。现象:在普通事件回调函数里调用
useState,报“只能在函数组件或自定义 Hook 中调用”。
原因:该函数既不是 React 函数组件,也不是自定义 Hook。
修法:把状态声明移到组件顶层,普通回调里只调用setState。现象:把
useState写在if或for中,运行报 Hook 数量/顺序相关错误。
原因:条件或循环会导致 Hook 调用数量不确定,破坏 Hook 调用顺序。
修法:Hook 一律放在组件顶层无条件调用;条件逻辑放到回调或渲染分支里。现象:
localStorage.setItem('list', obj)后读到[object Object],或JSON.parse报错。
原因:localStorage 只能存字符串;对象/数组未序列化,读取时也未反序列化或未处理空值。
修法:存用JSON.stringify(obj),读用JSON.parse(str),并对null、非法 JSON 做兜底。现象:组件卸载后定时器还在跑、鼠标事件仍触发,甚至出现重复绑定。
原因:挂载时添加了定时器/事件订阅,卸载时没有清理。
修法:useEffect返回清理函数,或类组件在componentWillUnmount中clearInterval/removeEventListener。现象:自定义 Hook 不叫
useXxx,内部调用 Hook 报错或规则识别异常。
原因:React 依靠use开头且后接大写的命名识别 Hook。
修法:统一命名为usePosition、useFetch这类形式。
练习
复现“定时器 2 秒后只保留初始值”的案例,分别用
setMsg(msg + '-')和setMsg(prev => prev + '-')运行,记录两种输出差异并解释触发时机。下面代码在类组件中调用了
useRef,请改造成可运行的函数组件,并说明为什么不能只把useRef换成普通变量。jsxclass Test extends React.Component { ref = useRef(null); }实现
saveCart(cart)与loadCart():把数组存入 localStorage,读取时处理空值、非法 JSON,返回空数组兜底。编写
usePosition():返回{ x, y },组件挂载时监听mousemove,卸载时解绑;再用两个组件复用该 Hook。判断下列名称在 React 中更可能被当作“函数组件 / Hook / 普通函数”,并说明理由:
usePosition、UsePosition、position、Position、use_position。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | setMsg用法(上) | 12:44 |
| 2 | setMsg用法-回调函数(下) | 02:06 |
| 3 | hook函数使用原则 | 16:15 |
| 4 | 自定义hook应用-未封装前 | 08:54 |
| 5 | 自定义hook应用-封装后 | 03:32 |
小结
- 状态更新依赖旧值时,优先使用函数式
setState,避免闭包拿到旧值。 - Hook 的调用位置是硬约束:函数组件或自定义 Hook 的顶层,不能进类组件、普通函数、条件或循环。
useXxx命名不是风格问题,而是 React 识别 Hook 的规则。- localStorage 存对象/数组必须序列化,读取必须反序列化并做异常兜底。
- 副作用要成对管理:挂载添加定时器/订阅/事件,卸载必须清理。
- 自定义 Hook 适合复用“逻辑”,不是复用 UI;返回值按调用方需要设计。
- 排查状态异常时,先查闭包旧值、Hook 调用顺序和清理函数三件事。