React 18下用renderHook测试自定义useFetch Hook遇act警告问题
问题描述
我有一个自定义Hook useFetch,组件挂载时发起API请求,管理isLoading、isError、data、refetch等状态,代码如下:
const useFetch = (endpoint, options) => { const [data, setData] = useState(null); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(true); const [trigger, setTrigger] = useState(true); const triggerSearch = () => { setTrigger(!trigger); }; useEffect(() => { const fetchData = async () => { try { const response = await fetch( `${process.env.API_URL}${endpoint}` ); const json = await response.json(); setData(json); setIsLoading(false); } catch (error) { setError(error); setIsLoading(false); } }; fetchData(); }, [endpoint, trigger]); return { data, isLoading, error, triggerSearch, }; };
使用Jest和testing-library/react测试该Hook时,因为React 18不再支持testing-library/react-hooks包,无法用renderHook返回的waitForNextUpdate,改用act和waitFor后测试能通过,但出现以下警告:
Warning: An update to TestComponent inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
我的测试代码如下:
test("should make an API call on mount", async () => { const hook = renderHook(() => useFetch("/api")); await act(async () => { await waitFor(() => expect(fetch).toHaveBeenCalledTimes(1)); }); expect(fetch).toHaveBeenCalledTimes(1); expect(getAccessTokenSilently).toHaveBeenCalledTimes(1); expect(hook.result.current.data).toEqual({ message: "Test" }); expect(hook.result.current.isLoading).toEqual(false); expect(hook.result.current.error).toEqual(null); });
即使移除所有断言只调用renderHook,仍会出现相同警告,求解决方向。
解决方向
1. 移除手动包裹的act
React 18版本的@testing-library/react中,renderHook和waitFor已经自动集成了act的逻辑,手动嵌套act会导致状态更新时序冲突,直接去掉外层的act即可:
test("should make an API call on mount", async () => { const hook = renderHook(() => useFetch("/api")); // 直接使用waitFor,无需手动包裹act await waitFor(() => expect(fetch).toHaveBeenCalledTimes(1)); expect(fetch).toHaveBeenCalledTimes(1); expect(getAccessTokenSilently).toHaveBeenCalledTimes(1); expect(hook.result.current.data).toEqual({ message: "Test" }); expect(hook.result.current.isLoading).toEqual(false); expect(hook.result.current.error).toEqual(null); });
2. 确保异步Mock的正确性
如果mock的fetch或getAccessTokenSilently没有返回正确的resolved Promise,会导致Hook内的状态更新延迟,触发警告。提前完善Mock逻辑:
// 测试前初始化Mock beforeEach(() => { global.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve({ message: "Test" }), }) ); global.getAccessTokenSilently = jest.fn(() => Promise.resolve("mock-token")); }); // 测试后清理Mock afterEach(() => { jest.clearAllMocks(); });
3. 升级测试库版本
旧版@testing-library/react在React 18下存在兼容问题,升级到最新稳定版能修复内置的act相关适配问题:
npm install @testing-library/react@latest --save-dev
4. 调整Hook内状态更新顺序(可选)
将setIsLoading(false)放在状态更新的前面,确保加载状态先变更,避免异步操作完成后状态更新的时序问题:
const fetchData = async () => { try { const response = await fetch(`${process.env.API_URL}${endpoint}`); const json = await response.json(); // 先更新加载状态,再更新数据 setIsLoading(false); setData(json); } catch (error) { setIsLoading(false); setError(error); } };
内容的提问来源于stack exchange,提问作者Berkeli

