当前位置: 首页 > news >正文

useState 真的那么简单吗?我在项目里踩过的坑

我敢打赌,你一定遇到过这种情况:

某天下午,同事在群里问:"咋回事啊,用户点了按钮,状态改了,但列表没更新啊?"

你开始调试,F12 打开,state 里的数据明明改了,UI 就是没反应。折腾半小时,最后发现——是直接改了数组,没有创建新对象。

或者这样:你看到前辈写的代码里,useState 十来个,各种奇怪的副作用代码到处都是,改一个字段牵一发动全身。最后索性不敢动,怕出bug。

我也是这样过来的。慢慢才明白,useState 看似简单,但很多人用了好几年也没真正吃透。

我刚工作时的懵逼时刻

那时候我对 useState 的理解就是:"就是个变量呗,用 setCount 改改值。"

const [count, setCount] = useState(0);function handleClick() {setCount(count + 1);console.log(count);  // 我以为会是 1
}

结果呢?还是 0。

我就很纳闷啊,为什么我改了还是 0?我甚至问过 ChatGPT(那时候还没有,我查的文档)。文档里说"状态更新是异步的",我当时理解得一知半解——"异步","什么鬼?为什么要异步?"

后来在大佬的指点下才明白:

React 不是你改了就立刻生效的。 你调用 setCount 时,你只是告诉 React:"嘿,请帮我把这个事儿加到待办清单里。" React 会合并你的所有更新,一起处理,然后才重新渲染页面。

当前这一帧里,count 的值是冻住的。你读不到新值。只有下一帧重新渲染时,你才能看到新的 count

想象一下,你在银行存钱。你告诉柜员:"我要存 100 块。" 柜员记录下来,然后排队处理了你和其他 10 个人的请求,一起更新系统。你不能指望她立刻告诉你新余额——还得等系统更新完。

setState 之后立刻读值?真正的坑来了

我第一次被这个坑害惨了。那是一个下午,做一个秒杀活动的功能:

function handleBuy() {// 用户点击"立即下单"setCount(count - 1);// 我以为这里 count 已经改了if (count <= 0) {// 库存不足,弹窗提示alert('已售罄');}
}

结果呢?用户一直能点"下单",count 怎么都减不完。

因为每次我判断的 count,都是上一帧的值。我在 setState 之后立刻判断,用的还是旧的 count

后来在测试提bug的时候才发现——这和我们的库存系统不一致!

我学到的第一课:不要试图在 setState 之后立刻用新值。把你的逻辑分开。如果你需要根据新状态做什么事,放到下一个组件渲染周期里,或者用 useEffect

// 正确的做法
function handleBuy() {
const newCount = count - 1;setCount(newCount);// 不在这里判断,而是在组件渲染时判断
}// 或者用 useEffect 监听
useEffect(() => {
if (count <= 0) {alert('已售罄');}
}, [count]);

多次 setState,为什么只有最后一个生效?

我还遇到过更尴尬的事儿。

我们的活动页面有个礼券码兑换的功能。用户输入一个优惠码,我需要做三件事:

  1. 验证码的有效性

  2. 获取折扣信息

  3. 更新用户的优惠券列表

我一开始这样写的:

function redeemCoupon(code) {setIsPending(true);setError(null);setDiscount(null);// 验证并获取
const result = await validateCoupon(code);if (result.success) {setCoupons(result.coupons);  // 更新券列表setDiscount(result.discount);  // 设置折扣} else {setError(result.message);  // 设置错误}setIsPending(false);
}

这看起来没问题,但实际场景更复杂。后来我们发现,如果用户快速操作(网络稍微慢一点),前面的状态会被后面的覆盖。

我就很郁闷啊,明明设置了啊,为什么没有?

真相是:我多次调用 setXxx,React 会合并这些更新。但每次都用的是同一个快照的旧值

比如说,我有个递增的需求:

function increment() {setCount(count + 1);setCount(count + 1);setCount(count + 1);
}

结果只加了 1,不是 3。因为三行代码用的都是同一个 count 值。相当于:

setCount(5 + 1);   // 6
setCount(5 + 1);   // 还是 6
setCount(5 + 1);   // 还是 6,最后取最后一个

后来我的前辈教我用函数式更新

function increment() {setCount(c => c + 1);  // React 给我最新的值setCount(c => c + 1);  // React 再给我新的值setCount(c => c + 1);  // React 再给我新的值
}

这样每次 React 都会把最新的值传给我,我这样操作就行了:

setCount(c => c + 1);

这个单独一行,看似简单,但威力巨大。

从"列表管理"学到的状态分组智慧

我们有个后台系统,要管理一个用户列表。一开始我就这样做:

const [users, setUsers] = useState([]);
const [totalCount, setTotalCount] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [currentPage, setCurrentPage] = useState(1);

五个 state,感觉很"完善"。

但问题来了。有一次,我在删除用户的时候,更新了 users 列表,但忘了更新 totalCount。结果用户界面显示的总数和实际列表数不符。

我坐在那里狂敲代码调试,最后发现——我在三个地方都需要同步这两个值。改一个忘一个。

后来我看到老大哥怎么做的,才恍然大悟:

// 按照"更新频率"和"相关性"分组
const [listData, setListData] = useState({
users: [],
totalCount: 0
});const [pagination, setPagination] = useState({
currentPage: 1,
pageSize: 10
});const [uiState, setUiState] = useState({
isLoading: false,
error: null
});

这样分组的好处是:

  • listData 总是一起更新,你不会忘记同步 users 和 totalCount

  • pagination 独立变化,改页码时不会影响其他

  • uiState 是通用的加载态和错误态,可以复用

更新时就变得清晰了:

function fetchUsers() {setUiState({ isLoading: true, error: null });api.getUsers(pagination.currentPage).then(res => {setListData({users: res.data,totalCount: res.total});setUiState({ isLoading: false, error: null });}).catch(err => {setUiState({ isLoading: false, error: err.message });});
}

再也不会出现数据不一致的问题了。

那个"衍生数据"的坑,差点被我重复踩

有一次,我在做一个购物车页面。用户可以添加/删除商品,我需要显示:

  1. 购物车里的商品列表

  2. 购物车总价

  3. 商品数量

一开始我"聪明"地这样做:

const [items, setItems] = useState([]);
const [totalPrice, setTotalPrice] = useState(0);
const [itemCount, setItemCount] = useState(0);function addItem(product) {
const newItems = [...items, product];setItems(newItems);setTotalPrice(totalPrice + product.price);  // 我自己维护总价setItemCount(itemCount + 1);  // 我自己维护数量
}function removeItem(productId) {
const removed = items.find(i => i.id === productId);setItems(items.filter(i => i.id !== productId));setTotalPrice(totalPrice - removed.price);setItemCount(itemCount - 1);
}

你能看出问题吗?

我在三个不同的地方维护着三份"真相"。只要有任何一个地方出错,整个购物车就乱套。

果然,后来有个 bug 出现了:用户点击"全选"之后,数量显示和实际列表对不上。我排查了半天,发现是某个角落的代码没有正确更新 itemCount

那时候我才意识到——我根本不需要存这些值

后来我改成:

const [items, setItems] = useState([]);// 这些都是计算出来的,不需要 state
const itemCount = items.length;
const totalPrice = items.reduce((sum, item) => sum + item.price, 0);function addItem(product) {setItems([...items, product]);
// itemCount 和 totalPrice 会自动"更新"
}function removeItem(productId) {setItems(items.filter(i => i.id !== productId));
// itemCount 和 totalPrice 会自动"更新"
}

这样就再也不会出现同步问题了。因为根本没有多个"真相源"。

那次异步 setState 教给我的

我们有一个"点赞"功能。用户点点赞按钮,我们要:

  1. 立刻改变 UI(点赞按钮变亮)

  2. 同时发送请求给服务器

我最初是这样写的:

const [liked, setLiked] = useState(false);function handleLike() {setLiked(!liked);// 发送请求api.addLike(postId).catch(err => {// 如果失败,恢复状态setLiked(liked);  // 这里有问题!});
}

看出来了吗?我在 .catch() 里用的还是旧的 liked 值

假设用户很快点了点赞,然后又点了取消。现在 liked 是 false。但如果第一个请求失败了,我的 catch 回调会把它改回 true(用的是当时的旧快照)。结果用户的操作就被反转了。

这是个经典的闭包陷阱。

正确做法是用函数式更新:

function handleLike() {setLiked(prev => !prev);api.addLike(postId).catch(() => {setLiked(prev => !prev);  // 恢复前一个状态});
}

这样不管发生了什么,我都是基于"最新的状态"来操作,不会出错。

计算型初始值,一个看不见的性能漏洞

我们有个很复杂的仪表板。首次加载时,需要做一堆初始化:处理大量数据、生成图表配置、诸如此类的。

我一开始这样做:

const [config, setConfig] = useState(generateComplexConfig(rawData));

问题是:每次这个组件重新渲染,generateComplexConfig 都会被调用一遍。

虽然 React 最后不会真的用这个返回值(它只用第一次的),但 JavaScript 还是浪费了 CPU 去计算。在我们这个场景里,这个函数要跑两秒钟。组件每次重新渲染都要卡两秒,那就离谱了。

后来老大哥教我一个技巧:

const [config, setConfig] = useState(() => generateComplexConfig(rawData));

只需要包装成一个函数,React 就只会在初始化时调用它。之后重新渲染时,它就不会再调用这个函数了。

这叫"懒初始化"。看起来简单,但对性能的影响能很显著。

真正的高手知道什么时候不用 useState

我刚工作时,什么东西都想放在 state 里。结果组件到处都是 useState,到处都是 re-render,到处都是 useEffect 来同步各种奇怪的东西。

后来我才学会问自己一个问题:这个东西真的需要是 state 吗?

比如说,我们有一个表单,用户不断地输入。每输入一个字符,我都在计算"还能输入多少字符"。

// ❌ 我最初想这样做
const [text, setText] = useState('');
const [remainingChars, setRemainingChars] = useState(100);function handleChange(e) {const newText = e.target.value;setText(newText);setRemainingChars(100 - newText.length);
}

但这样的话,输入框每次改变都会触发两次 state 更新,两次 re-render。

实际上:

// ✅ 正确做法
const [text, setText] = useState('');// 直接算,不需要 state
const remainingChars = 100 - text.length;function handleChange(e) {setText(e.target.value);
}

一行代码搞定,而且根本没有多余的 re-render。

还有,我之前想用 state 来存一个"用户操作了没"的标志,用来控制要不要显示某个提示:

// ❌ 不需要
const [hasOpened, setHasOpened] = useState(false);if (someCondition && !hasOpened) {showTip();setHasOpened(true);
}

这会让组件重新渲染一遍(虽然 UI 可能不会变)。其实我只需要:

// ✅ 用 useRef
const hasOpenedRef = useRef(false);if (someCondition && !hasOpenedRef.current) {showTip();hasOpenedRef.current = true;
}

ref 改变时不会触发 re-render,所以这里用它最合适。

写在最后

我和你说这些,不是为了装逼。而是想让你知道:

我也是从各种坑里爬出来的。

每一个"原来是这样"的时刻,都是在项目里被 bug 追着跑的时候学到的。

所以如果你现在写的代码不够完美,项目里还有各种 setState 的问题,这很正常。关键是要去理解——为什么会这样?为什么 React 要异步更新?为什么不能直接改对象?

一旦你真正理解了这些原理,不是背下来,而是在项目里用过几次,踩过几个坑,那么回头看你最开始的代码,你就会笑出声来。

然后你会开始写出更清晰、更少bug、更好维护的代码。

这就是进步。

http://www.dtcms.com/a/569365.html

相关文章:

  • 如何用5种实用方法将电脑上的音乐传输到安卓手机
  • 做网页到哪个网站找素材物流网站有哪些
  • MP4视频播放问题
  • HR8837:赋能低压直流电机的高效安全驱动芯片
  • Linux源码安装FFmpeg和av库
  • 亳州市城乡建设局网站ps设计网站首页效果图
  • Syncthing Linux 部署教程
  • 做疏通什么网站推广好网页制作软件 ad
  • html 和css基础常用的标签和样式(2)-css
  • 【数据集】【YOLO】【目标检测】共享单车数据集,共享单车识别数据集 3596 张,YOLO自行车识别算法实战训推教程。
  • Coze-AI智能体开发平台5-Coze的API与SDK
  • 河南网站建设优化技术网站建设与维护学什么科目
  • 超越简单的回放:深度解析国标GB28181算法算力平台EasyGBS的录像检索与回放技术
  • HCIP Datacom 认证难度高吗?零基础能考吗?
  • 代码实战:PHP爬虫抓取信息及反爬虫API接口
  • CentOS 7 停止维护后 YUM 源配置速查手册
  • TypeScript 类型系统 ------公司项目实战 + 面试通关指南
  • 东莞网络网站建设深圳建设局网站注册结构师培训
  • 做网站推广链接该怎么做那曲地区建设局网站
  • AI研究-120 DeepSeek-OCR 从 0 到 1:上手路线、实战要点
  • 2025,5月试卷|错题笔记
  • Syslog基础详解:协议、服务器、端口和实时监控
  • rk3568-android11-wifi-aic8800
  • 东城区网站排名seo网站 动态 静态
  • 网站就业技术培训机构seo需要掌握什么技能
  • CUDA C++编程指南(4)——硬件实现
  • Nacos集群部署实战:3节点+Nginx+MySQL高可用方案
  • 深入理解五种 IO 模型与非阻塞 IO:从原理到场景选型
  • 大专生升学与职业发展路径探析:从专升本到能力进阶
  • 12. PLC与继电器控制系统的区别