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尚未被覆盖。
解决方案
前端:阻止重复请求
有两种可行方案:
临时关闭Strict Mode
在index.tsx中移除<React.StrictMode>的包裹,这样useEffect只会执行一次。但不推荐长期使用,Strict Mode能帮助提前发现潜在的副作用问题。添加请求锁控制
在组件内维护请求状态,避免重复发起请求:
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刷新逻辑
调整验证逻辑,避免因重复请求导致的失败:
- 简化查询条件
由于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)。
- 使用原子更新操作
若必须验证旧refresh token,可采用数据库原子更新操作,确保仅当旧token匹配时才更新。这种情况下,即使两次请求,也只有第一次会成功,第二次返回失败,但前端已拿到新token,不会影响用户体验。
内容的提问来源于stack exchange,提问作者amirhosein karimi

