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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.24 10:18:31