You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React开发环境useEffect调用两次致/tokens接口偶发查询失败

问题分析与解决方案

问题背景

基于Express.js开发的API采用JWT的access/refresh token实现认证,前端React 18.2.0将refresh token存入HttpOnly Cookie,用户数据(含access token)存在Redux内存中。用户离开页面后Redux数据清空,返回时会携带Cookie调用/tokens接口刷新token,无需跳转登录页。但开发环境下useEffect会执行两次,导致接口被请求两次:第一次能成功生成新token并更新数据库中的refresh_token,第二次请求偶发因旧refresh_token匹配失败返回401,有时两次又均能成功。

前端代码片段

useEffect(() => {
  if (user === undefined) {
    axios.post('/tokens').then((result: any) => {
      dispatch(addUser(result.data.user));
    }).catch((error: any) => {
      dispatch(addUser(undefined))
    })
  }
}, []);

React 18开发环境的Strict Mode会强制执行两次无依赖的useEffect,以此检测副作用的稳定性,这是导致重复请求的直接原因。

后端代码片段

tokens = async (req: Request, res: Response, next: NextFunction): Promise<Response> => {
  const refreshToken = req.headers.cookie?.split('=')[1];

  const user: any[] = await this._userRepository.find({
      fields: [
          {
              field: {
                  username: req.user.username,
                  email: req.user.email,
                  id: req.user.id,
                  refresh_token: refreshToken
              },
              op: 'AND'
          }
      ]
  });

  if (user[0] !== undefined) {
      const newAccessToken: string = generateJwt('access_token', user[0], '1m');
      const newRefreshToken: string = generateJwt('refresh_token', user[0], '10d');

      await this._userRepository.update({
          refresh_token: newRefreshToken,
      }, {
          fields: [
              {
                  field: {
                      id: req.user.id,
                      username: req.user.username,
                      email: req.user.email,
                      refresh_token: refreshToken
                  },
                  op: 'AND'
              }
          ]
      });

      const data: Record<string, any> = {
          status: true,
          message: 'New tokens generated successfully',
          user: {
              access_token: newAccessToken,
              refresh_token: newRefreshToken,
          }
      }

      return res.json(data)
  }

  throw new BaseError('Unauthorized', 401);
}

第一次请求成功后,数据库中的refresh_token已更新为新值,第二次请求仍携带旧refresh_token,此时查询用户会失败;偶尔两次成功的情况,是因为数据库更新操作存在异步延迟,第二次请求时旧token尚未被覆盖。


解决方案

前端:阻止重复请求

有两种可行方案:

  1. 临时关闭Strict Mode
    在index.tsx中移除<React.StrictMode>的包裹,这样useEffect只会执行一次。但不推荐长期使用,Strict Mode能帮助提前发现潜在的副作用问题。

  2. 添加请求锁控制
    在组件内维护请求状态,避免重复发起请求:

const [isRefreshing, setIsRefreshing] = useState(false);

useEffect(() => {
  if (user === undefined && !isRefreshing) {
    setIsRefreshing(true);
    axios.post('/tokens')
      .then((result: any) => {
        dispatch(addUser(result.data.user));
      })
      .catch((error: any) => {
        dispatch(addUser(undefined))
      })
      .finally(() => {
        setIsRefreshing(false);
      });
  }
}, [user, isRefreshing]);

通过isRefreshing状态控制,即使useEffect执行两次,也只会发起一次请求。

后端:优化refresh token刷新逻辑

调整验证逻辑,避免因重复请求导致的失败:

  1. 简化查询条件
    由于req.user已经是通过旧refresh token解析出的合法用户,无需再用旧refresh_token匹配数据库,仅通过用户ID查询即可:
// 查询用户
const user: any[] = await this._userRepository.find({
    fields: [
        {
            field: { id: req.user.id },
            op: 'AND'
        }
    ]
});

// 更新refresh_token
await this._userRepository.update({
    refresh_token: newRefreshToken,
}, {
    fields: [
        {
            field: { id: req.user.id },
            op: 'AND'
        }
    ]
});

这样只要req.user合法,就能成功刷新token,后续请求会自动使用新的refresh token(若前端已更新Cookie)。

  1. 使用原子更新操作
    若必须验证旧refresh token,可采用数据库原子更新操作,确保仅当旧token匹配时才更新。这种情况下,即使两次请求,也只有第一次会成功,第二次返回失败,但前端已拿到新token,不会影响用户体验。

内容的提问来源于stack exchange,提问作者amirhosein karimi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 19:02:54