React-hooks-testing-library: Deal with functions passed to hooks which introduce state-change

Created on 19 Sep 2019  路  2Comments  路  Source: testing-library/react-hooks-testing-library

What is your question: How to deal with functions passed to hooks which introduce state-change?

I have sth like the following hook:

function useHook(req: () => Promise<number>) {
    const [val, setVal] = useState(0);

    const request = useCallback(async () => {
        const x = await req();

        setVal(x);
    }, []);

    useEffect(() => {
        request();
    }, []);

    return { setVal, val };
}

const { result } = renderHook(() => useHook(async () => 100));

calling this results in an error

Warning: An update to TestHook inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):

The reason is that the function-param returning the actual value, which will be set as a state inside the hook, is not happening inside the hook. I didn't know that this really matters, but it seems like it does. Leaving out const x = await req(); causes the hook to work just fine in the test.
What kind of helps is wrapping everything inside act():

async function renderHookAct<T>(
    action: () => T,
    options?: RenderHookOptions<T>
) {
    let hookResult: HookResult<T> | undefined;

    await act(async () => {
        const { result } = renderHook(action, options);

        hookResult = result;
    });

    if (hookResult) {
        return hookResult.current;
    }

    throw new Error('HookResult is not defined');
}

const { result } = renderHookAct(() => useHook(async () => 100));

But by doing so, there will be other side-effect. for example when trying to change the state inside the hook again (like with an exported method from the hook), it will throw the warning Warning: An update to TestHook inside a test was not wrapped in act(...). again.

question

Most helpful comment

I believe if you add an await waitForNextUpdate() after the renderHook call, you should be ok.

const { result, waitForNextUpdate } = renderHook(() => useHook(async () => 100));

await waitForNextUpdate();

Here is a sandbox showing no error in the console.

Please see the Async hook docs for more info.

when trying to change the state inside the hook again (like with an exported method from the hook), it will throw the warning Warning: An update to TestHook inside a test was not wrapped in act(...). again.

Any external call that updates the state will need to be wrapped in act, e.g.

const { result, waitForNextUpdate } = renderHook(() => useHook(async () => 100));

await waitForNextUpdate();

act(() => {
  result.current.setVal(200)
})

This is just a restriction of react. Without knowing what funcitons your hook is going to return, there is little this library can do to automatically wrap the calls with act automatically for you (although I'm open to ideas if anyone has any).

Also, there are some issue with your current hook implementation. I'm not sure how much of it is just as a sample vs your actual hook, but useCallback should be using req in it's dependencies in some way, as should useEffect be using request (useCallback does not guarantee to always return the same instance). Here is some useful articles from the React docs on the topic.

All 2 comments

I believe if you add an await waitForNextUpdate() after the renderHook call, you should be ok.

const { result, waitForNextUpdate } = renderHook(() => useHook(async () => 100));

await waitForNextUpdate();

Here is a sandbox showing no error in the console.

Please see the Async hook docs for more info.

when trying to change the state inside the hook again (like with an exported method from the hook), it will throw the warning Warning: An update to TestHook inside a test was not wrapped in act(...). again.

Any external call that updates the state will need to be wrapped in act, e.g.

const { result, waitForNextUpdate } = renderHook(() => useHook(async () => 100));

await waitForNextUpdate();

act(() => {
  result.current.setVal(200)
})

This is just a restriction of react. Without knowing what funcitons your hook is going to return, there is little this library can do to automatically wrap the calls with act automatically for you (although I'm open to ideas if anyone has any).

Also, there are some issue with your current hook implementation. I'm not sure how much of it is just as a sample vs your actual hook, but useCallback should be using req in it's dependencies in some way, as should useEffect be using request (useCallback does not guarantee to always return the same instance). Here is some useful articles from the React docs on the topic.

awesome, thanks a lot for the effort.
waitForNextUpdate actually solves it for me.

Also, there are some issue with your current hook implementation. I'm not sure how much of it is just as a sample vs your actual hook, but useCallback should be using req in it's dependencies in some way, as should useEffect be using request (useCallback does not guarantee to always return the same instance). Here is some useful articles from the React docs on the topic.

It was just for the sample like this

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Kiiiwiii picture Kiiiwiii  路  3Comments

ntucker picture ntucker  路  6Comments

kentcdodds picture kentcdodds  路  6Comments

DaveStein picture DaveStein  路  7Comments

SachinSharmaE picture SachinSharmaE  路  6Comments