如何测试含返回数据自定义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); }); });
原因分析
自定义Hook的无限触发逻辑
useFetchHook中,useEffect的依赖数组包含了外部导入的data(来自../data),同时在useEffect内部又调用setData(data)更新Hook的state。如果导入的data是引用类型(对象/数组),每次组件重渲染时,这个导入的data会被React判定为新的引用,导致useEffect反复触发。每次触发都会更新Hook的state,进而让Hook返回新的data对象,触发组件的useEffect执行。组件与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
相关产品推荐
相关产品推荐

