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

NextJS 13中React useEffect在Jest测试中无限循环问题

问题

我用NextJS 13写了一个Members组件,里面的useEffect依赖searchParams触发数据获取。用Jest测试时,mock了next/navigation的useSearchParams,但测试超时,看起来是useEffect无限循环了,怀疑是mock的get方法导致的,该怎么修改这个mock?

组件代码

export default function Members() {
  // States
  const [users, setUsers] = useState<MemberType[]>([]);
  const [loading, setLoading] = useState(false);
  const searchParams = useSearchParams();
  // To refresh the page
  const router = useRouter();
  const inputRef = useRef<HTMLInputElement | null>(null);

  // Function to fetch a book
  const fetchBooks = async (userQuery: string | null) => {
    if (!userQuery) {
      const res = await fetch("/api/users/members", {
        method: "GET",
        headers: { "Content-Type": "application/json" },
      });
      const body: MemberType[] = await res.json();
      return body;
    }
    const res = await fetch(`/api/users/members/${userQuery}`, {
      method: "GET",
      headers: { "Content-Type": "application/json" },
    });
    const body: MemberType[] = await res.json();
    return body;
  };

  // Runs if the search parameters change
  useEffect(() => {
    setLoading(true);
    const searched = searchParams.get("search");
    console.log(searched);
    
    fetchBooks(searched).then((users) => {
      setUsers(users);
      setLoading(false);
    });
    if (searched && inputRef.current) {
      inputRef.current.value = searched;
    }
  }, [searchParams]);
  //...

测试文件中的mock

jest.mock("next/navigation", () => ({
  useRouter: () => ({
    push: jest.fn(),
  }),
  useSearchParams: () => ({
    get: jest.fn().mockReturnValue(''),
  }),
}));

测试代码

describe("Members page", () => {
  it("should render the page correctly", async () => {
    await act(async () => {
      render(<Members />);
      
    });
  });
});
解决方案

问题根源是你mock的useSearchParams每次调用都会返回新的对象实例,而React的useEffect依赖对比是引用级别的——组件每次渲染时,searchParams都是全新的对象,导致useEffect反复触发,形成无限循环。

具体修复步骤:

  • 让searchParams返回稳定的引用
    把mock的searchParams对象提取到mock函数外部,确保每次调用useSearchParams都返回同一个实例:

    // 定义稳定的searchParams实例,放在mock外部
    const mockSearchParams = {
      get: jest.fn().mockReturnValue(''),
    };
    
    jest.mock("next/navigation", () => ({
      useRouter: () => ({
        push: jest.fn(),
      }),
      // 每次调用都返回同一个对象
      useSearchParams: () => mockSearchParams,
    }));
    
  • mock fetch请求避免异常
    组件里的fetchBooks会发起真实网络请求,测试时不mock的话可能导致超时或失败,全局mock fetch:

    // 在测试文件顶部添加fetch mock
    global.fetch = jest.fn(() =>
      Promise.resolve({
        json: () => Promise.resolve([]), // 模拟接口返回空用户列表
        ok: true,
      })
    );
    
    // 测试后清除mock,避免用例间污染
    afterEach(() => {
      jest.clearAllMocks();
    });
    
  • 简化测试代码
    React Testing Library的render已经自动处理了act的异步逻辑,不需要手动包裹:

    describe("Members page", () => {
      it("should render the page correctly", async () => {
        render(<Members />);
        // 可以添加断言验证组件状态,比如等待loading结束
        await waitFor(() => {
          expect(screen.queryByText('加载中')).not.toBeInTheDocument();
        });
      });
    });
    

扩展测试场景

如果要测试不同搜索参数的情况,可以在单个用例中修改mock的返回值:

it("should fetch filtered users when search param exists", async () => {
  // 修改mock返回特定搜索值
  mockSearchParams.get.mockReturnValue('test-user');
  render(<Members />);
  // 断言fetch调用了正确的接口地址
  await waitFor(() => {
    expect(global.fetch).toHaveBeenCalledWith('/api/users/members/test-user', expect.any(Object));
  });
});

内容的提问来源于stack exchange,提问作者Danny Yuan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 14:10:53