React-hooks-testing-library: How to verify multiple calls to state in a test

Created on 8 Nov 2019  路  4Comments  路  Source: testing-library/react-hooks-testing-library

I am trying to call multiple calls to decrement counter hook in a same test. It's crucial for me to be able to verify the result after multiple calls.
Here is my implementation code.

export default function useCount({initial=0})
{
    const [count,setCount]=React.useState(initial);
    const increment = React.useCallback( ()=>{setCount(count+1)},[]);
    const decrement =React.useCallback( ()=>{
        setCount(count-1);
    },[]);
    const reset = ()=>{};
    return {count,increment,decrement,reset};


}

and here is my test code

const getProps = (props={})=>{
    return {...props,initial:0};
}
const setup  = () =>{
    return UseCount(getProps());
}
   test("should decrement",   ()=>{
        const {result,waitForNextUpdate,rerender} = renderHook ( ()=>setup());
        act(  ()=>{

            result.current.decrement();
            result.current.decrement();
        })
        expect(result.current.count).toBe(-2);
   });
});

In the results even though i can verify calls are being made to decrement function, however, it seems state is updated only one time(specifically the last time).

Can you tell me what needs to be done?

Here is my react and testing version

 "react": "^16.11.0",
  "@testing-library/react-hooks": "^3.2.1",
question

Most helpful comment

Hi @SachinSaggezza,

It is a common mistake when updating state that relies on previous state value to define the update functions as you have here:

    const decrement =React.useCallback( ()=>{
        setCount(count-1);
    },[]);

The issue here is that state updates within the same act block will get batched together and count does not change until after the updates have been flushed, so every call within the batched changes will effectively be setCount(0-1), resulting in count being-1after theact` block is finished.

Luckily, setCount (or more specifically, any setter returned from useState) can also accept a callback that passes in intermediary state values for exactly this use case. Changing the definition to:

    const decrement =React.useCallback( ()=>{
        setCount((x) => x-1);
    },[]);

should see your test going green. In this version x will always be the current count value, even when the updates have been batched.

All 4 comments

Hi @SachinSaggezza,

It is a common mistake when updating state that relies on previous state value to define the update functions as you have here:

    const decrement =React.useCallback( ()=>{
        setCount(count-1);
    },[]);

The issue here is that state updates within the same act block will get batched together and count does not change until after the updates have been flushed, so every call within the batched changes will effectively be setCount(0-1), resulting in count being-1after theact` block is finished.

Luckily, setCount (or more specifically, any setter returned from useState) can also accept a callback that passes in intermediary state values for exactly this use case. Changing the definition to:

    const decrement =React.useCallback( ()=>{
        setCount((x) => x-1);
    },[]);

should see your test going green. In this version x will always be the current count value, even when the updates have been batched.

Hi,
Thanks for quick reply, so basically for batching multiple state change requests in act callback, in the implementation we need to account for intermediary state values?

The intermediary states are caused any time your callbacks would be called multiple times before rendering has completed. The act block you have is behaving the same way that a component using your hook would if it called decrement twice in the same event handler or in it's own useEffect hook. I believe issues like this will become even more apparent when concurrent mode becomes mainstream and suspending components will cause renders to take longer to complete (but I'm still quite early in my learning on these concepts so might be off target on this one).

An alternative to changing your implentation would be to seperate the decrement calls into seperate act blocks, essentially unbatching them for your test

   test("should decrement",   ()=>{
        const {result,waitForNextUpdate,rerender} = renderHook ( ()=>setup());
        act(()=>{
            result.current.decrement();
        });
        act(()=>{
            result.current.decrement();
        });
        expect(result.current.count).toBe(-2);
   });

I still recommend handling the intermediary states in your implementation over this as it will help prevent subtle bugs from being introduced to the hooks consumers.

I'll consider this resolved for now, but feel free to comment if you still have questions.

Was this page helpful?
0 / 5 - 0 ratings