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

如何测试含返回数据自定义Hook的组件且避免无限重渲染

无限重渲染警告原因分析与解决

问题背景

尝试为使用自定义Hook获取数据的组件编写单元测试时遇到以下问题:

  • 组件的useEffect未添加fetchedData依赖时,页面无法渲染数据
  • 添加fetchedData到依赖数组后,测试出现无限重渲染警告:Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.

相关代码如下:

组件代码

const Component = () => {
  // This state stores the array of recipes with the changes performed by the customer.
  const [data, setData] = React.useState([]);
  const { data: { fetchedData }, loading } = useFetchHook();

  React.useEffect(() => {
    if (fetchedData) {
      setData(fetchedData);
    }
  }, []);

  return (
    <>
      <Row>
        {data.map((recipe) => (
          <Col data-testid={'list-item'} key={data.id}>
            <Box mb="md">
              <Card
                {...data}
              />
            </Box>
          </Col>
        ))}
      </Row>
    </>
  );
};

export default Component;

自定义Hook代码

import { useState, useEffect } from 'react';
import data from '../data';

const useFetchHook = () => {
  const [data, setData] = useState({});
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const timer = setTimeout(() => {
      setData(data);
      setLoading(false);
    }, 1000);

    return () => clearTimeout(timer);
  }, [data]);

  return { data, loading };
};

export default useFetchHook;

测试代码

jest.mock("../../hooks/useFetchHook", () => ({
  __esModule: true,
  default: () => ({  
    loading: false, 
    data: {
      data: [
        {
          /////
        },
      ]
    } 
  }),
}));

it('renders the list', async() => {
  act(() => {
    render(<Component/>);
    expect(screen.queryAllByTestId('list-item').length).toBe(1);
  });
});

原因分析

  1. 自定义Hook的无限触发逻辑
    useFetchHook中,useEffect的依赖数组包含了外部导入的data(来自../data),同时在useEffect内部又调用setData(data)更新Hook的state。如果导入的data是引用类型(对象/数组),每次组件重渲染时,这个导入的data会被React判定为新的引用,导致useEffect反复触发。每次触发都会更新Hook的state,进而让Hook返回新的data对象,触发组件的useEffect执行。

  2. 组件与Hook的循环触发
    当组件的useEffect添加fetchedData作为依赖后,只要useFetchHook返回的fetchedData变化,组件就会调用setData更新自身state,触发组件重渲染。而useFetchHook因为自身的问题不断生成新的data对象,导致组件的useEffect被反复触发,形成组件重渲染 → Hook触发useEffect → 组件再重渲染的无限循环。

解决方案

1. 修复自定义Hook的useEffect逻辑

useFetchHook的目的是模拟一次数据请求,只需要在初始化时执行一次,因此将useEffect的依赖数组改为空数组,同时避免变量名混淆(将导入的data重命名为initialData):

import { useState, useEffect } from 'react';
import initialData from '../data';

const useFetchHook = () => {
  const [data, setData] = useState({});
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const timer = setTimeout(() => {
      setData(initialData);
      setLoading(false);
    }, 1000);

    return () => clearTimeout(timer);
  }, []); // 空数组仅初始化执行一次

  return { data, loading };
};

export default useFetchHook;

2. 修正组件的依赖与循环渲染问题

  • 组件的useEffect添加fetchedData作为依赖,此时因为Hook只会返回一次稳定的fetchedData,不会反复触发useEffect:
React.useEffect(() => {
  if (fetchedData) {
    setData(fetchedData);
  }
}, [fetchedData]); // 添加正确的依赖
  • 修正组件map循环的key错误:将key={data.id}改为key={recipe.id},避免因key重复导致的渲染异常:
{data.map((recipe) => (
  <Col data-testid={'list-item'} key={recipe.id}>
    {/* ... */}
  </Col>
))}

3. 优化测试代码(可选)

测试中使用act包裹渲染逻辑时,建议配合waitFor处理异步逻辑(因为Hook有setTimeout),确保断言时机正确:

it('renders the list', async () => {
  render(<Component/>);
  await waitFor(() => {
    expect(screen.getAllByTestId('list-item').length).toBe(1);
  });
});

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.11 12:31:01