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

React组件重复渲染引发Spring Boot API重复请求及主键约束异常问题排查

解决React useFetch钩子重复请求导致的API约束错误问题

看起来你遇到的是React开发环境下Strict Mode引发的重复请求问题,再加上依赖项引用不稳定的推波助澜,最终导致Spring Boot后端收到重复的创建请求,触发主键重复的约束异常。下面是具体的分析和解决方案:

问题根源分析

  1. React 18 Strict Mode的特性:在开发环境中,React的Strict Mode会自动执行两次useEffect(以及其他生命周期函数),目的是帮助你检测潜在的副作用问题。这是开发环境的专属行为,生产环境不会出现,但对于POST这类非幂等请求来说,直接导致了重复创建实体的问题。
  2. 依赖项引用不稳定:你的useEffect依赖数组中包含body参数,而body通常是一个对象。在组件每次渲染时,即使对象内容完全相同,它也会被重新创建为一个新的引用,这会让React认为依赖项发生了变化,从而再次触发useEffect,发起重复请求。

具体解决方案

1. 给useFetch添加请求取消机制(优先推荐)

使用AbortController来取消未完成的请求,当useEffect重新执行时,先终止之前的请求,避免重复发送:

import { useState, useEffect } from 'react';
export function useFetch(url, method, body, param, update) {
  const [data, setData] = useState({});
  const [isLoading, setLoading] = useState(true);
  const [error, setError] = useState(false);

  useEffect(() => {
    if (!url) return;
    // 创建AbortController实例
    const abortController = new AbortController();
    const signal = abortController.signal;

    setLoading(true)
    setError(false)
    var myHeaders = new Headers();
    myHeaders.append("Content-Type", "application/json");
    myHeaders.append("Connection", "Keep-alive");
    var myInit = {
      method: (method ? method : "GET"),
      headers: myHeaders,
      mode: 'cors',
      cache: 'default',
      body: (body ? JSON.stringify(body) : null),
      signal // 将信号传入请求配置
    };
    const allPath = "http://localhost:9000/streengeAPI" + url + (param ? param : "");

    async function fetchData() {
      try {
        const response = await fetch(allPath, myInit);
        if (signal.aborted) return; // 如果请求已取消,直接返回
        const data = await response.json();
        setData(data);
      } catch (err) {
        if (err.name !== 'AbortError') { // 忽略取消请求的错误
          console.log(err);
          setError(true);
        }
      } finally {
        if (!signal.aborted) { // 只有请求未取消时才更新状态
          setLoading(false);
        }
      }
    }

    fetchData();

    // 组件卸载或依赖变化时,取消请求
    return () => abortController.abort();
  }, [url, method, body, param, update])

  return { isLoading, data, error };
}

2. 稳定body参数的引用

如果body对象的内容不会频繁变化,可以在调用useFetch的组件中使用useMemo来缓存它,避免每次渲染都生成新的引用:

// 在调用useFetch的组件中
const body = useMemo(() => ({
  // 你的请求体内容,比如实体数据
  id: 'PR0019',
  ...其他字段
}), [/* 这里放body内容依赖的变量,比如表单输入值 */]);

// 然后调用useFetch
const { isLoading, data, error } = useFetch('/api/entities', 'POST', body, null, update);

3. 临时关闭Strict Mode(仅开发环境调试用)

如果你只是想快速验证问题是否由Strict Mode导致,可以暂时移除index.js中的<StrictMode>包裹:

// 原来的代码
root.render(
  <StrictMode>
    <App />
  </StrictMode>
);

// 修改后
root.render(
  <App />
);

⚠️ 注意:这只是临时调试手段,不建议在开发中长期关闭Strict Mode,它能帮你提前发现很多潜在问题。

4. 后端幂等性处理(补充保障)

从后端层面增加幂等性校验,比如要求前端传递唯一的请求ID,或者在创建实体前先检查主键是否已存在,返回友好的错误提示,避免直接抛出数据库约束异常:

// 示例:Spring Boot服务层代码
@Service
public class EntityService {
    @Autowired
    private EntityRepository repository;

    public Entity createEntity(Entity entity) {
        if (repository.existsById(entity.getId())) {
            throw new RuntimeException("实体ID已存在");
        }
        return repository.save(entity);
    }
}

通过以上方案,你可以彻底解决重复请求导致的主键约束错误问题,同时让你的useFetch钩子更加健壮。

内容的提问来源于stack exchange,提问作者Tadjou Steve

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.27 18:47:31