首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >useState 真的那么简单吗?我在项目里踩过的坑

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

作者头像
前端达人
发布2025-11-20 08:46:35
发布2025-11-20 08:46:35
810
举报
文章被收录于专栏:前端达人前端达人

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

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

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

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

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

我刚工作时的懵逼时刻

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

代码语言:javascript
复制
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 之后立刻读值?真正的坑来了

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

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

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

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

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

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

代码语言:javascript
复制
// 正确的做法
function handleBuy() {
const newCount = count - 1;
  setCount(newCount);

// 不在这里判断,而是在组件渲染时判断
}

// 或者用 useEffect 监听
useEffect(() => {
if (count <= 0) {
    alert('已售罄');
  }
}, [count]);

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

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

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

  1. 验证码的有效性
  2. 获取折扣信息
  3. 更新用户的优惠券列表

我一开始这样写的:

代码语言:javascript
复制
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 会合并这些更新。但每次都用的是同一个快照的旧值

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

代码语言:javascript
复制
function increment() {
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
}

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

代码语言:javascript
复制
setCount(5 + 1);   // 6
setCount(5 + 1);   // 还是 6
setCount(5 + 1);   // 还是 6,最后取最后一个

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

代码语言:javascript
复制
function increment() {
  setCount(c => c + 1);  // React 给我最新的值
  setCount(c => c + 1);  // React 再给我新的值
  setCount(c => c + 1);  // React 再给我新的值
}

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

代码语言:javascript
复制
setCount(c => c + 1);

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

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

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

代码语言:javascript
复制
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。结果用户界面显示的总数和实际列表数不符。

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

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

代码语言:javascript
复制
// 按照"更新频率"和"相关性"分组
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 是通用的加载态和错误态,可以复用

更新时就变得清晰了:

代码语言:javascript
复制
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. 商品数量

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

代码语言:javascript
复制
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

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

后来我改成:

代码语言:javascript
复制
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. 同时发送请求给服务器

我最初是这样写的:

代码语言:javascript
复制
const [liked, setLiked] = useState(false);

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

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

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

这是个经典的闭包陷阱。

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

代码语言:javascript
复制
function handleLike() {
  setLiked(prev => !prev);
  
  api.addLike(postId).catch(() => {
    setLiked(prev => !prev);  // 恢复前一个状态
  });
}

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

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

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

我一开始这样做:

代码语言:javascript
复制
const [config, setConfig] = useState(generateComplexConfig(rawData));

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

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

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

代码语言:javascript
复制
const [config, setConfig] = useState(() => generateComplexConfig(rawData));

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

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

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

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

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

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

代码语言:javascript
复制
// ❌ 我最初想这样做
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。

实际上:

代码语言:javascript
复制
// ✅ 正确做法
const [text, setText] = useState('');

// 直接算,不需要 state
const remainingChars = 100 - text.length;

function handleChange(e) {
  setText(e.target.value);
}

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

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

代码语言:javascript
复制
// ❌ 不需要
const [hasOpened, setHasOpened] = useState(false);

if (someCondition && !hasOpened) {
  showTip();
  setHasOpened(true);
}

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

代码语言:javascript
复制
// ✅ 用 useRef
const hasOpenedRef = useRef(false);

if (someCondition && !hasOpenedRef.current) {
  showTip();
  hasOpenedRef.current = true;
}

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

写在最后

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

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

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

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

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

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

这就是进步。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-11-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端达人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 我刚工作时的懵逼时刻
  • setState 之后立刻读值?真正的坑来了
  • 多次 setState,为什么只有最后一个生效?
  • 从"列表管理"学到的状态分组智慧
  • 那个"衍生数据"的坑,差点被我重复踩
  • 那次异步 setState 教给我的
  • 计算型初始值,一个看不见的性能漏洞
  • 真正的高手知道什么时候不用 useState
  • 写在最后
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档