Search code examples
destructuringreact-hooks-testing-library

Destructuring need waitFor


Why does destructuring need waitFor?

Codesandbox

All tests do the same thing. In the 'ok.test.ts' file I use renderHook, I use result.current[1] to set the state and result.current[0] to get the value of the state, so far so good.

In 'not-ok' tests I do destructuring the result of renderHook and the tests fail because the state value is incorrect.

In 'resolved.test.ts' when using waitFor the test with destructuring works. I understand that set states is async. I didn't understand why the 'ok.test.ts' works without waitFor and without destructuring, but if I do destructuring I need waitFor.


Solution

  • Author of react-hooks-testing-library here.

    TL;DR; you cant destructure result.current and have get receive updated values.

    This comes up a lot so I'll take some time to give a more detailed answer for anyone coming across this.

    Firstly, in your example, the resolved test is passing because waitFor returns a promise that your must await to see the failure:

    // ...
        await waitFor(() => {
          expect(get).toStrictEqual({
            data1: 1,
            data2: 2,
            data3: 3
          });
        });
    // ...
    

    In this case, it times out waiting for the expectation pass because the value never changes.

    So the real question is why isn't get (odd name BTW... state would be more appropriate in this example) updating when set is called?

    Well, let's look at this code:

    const result = {
      state: 0,
      setState(newState: number) {
        this.state = newState;
      }
    };
    
    const { state, setState } = result;
    
    setState(1);
    
    expect(state).toBe(1); // fails
    

    The test fails for the same reason as your examples. Can you see why?

    Well, The destructuring of result locks the value of result.state to whatever it was at that moment in a new variable called state. Calling setState (or result.setState) will successfully update result.state, but there is no link to the state variable so the value does not change.

    So by having const { result: { current: [get, set] } } = renderHook(...) you are also locking get to whatever the initial value of result.current has and not amount of setting is going to allow the new variable to be updated because it's connection to result.current has been lost.

    Finally, I see this as a common accident, especially when using tuple results from hooks (e.g. const [state, setState] = useState()). The common reason given is that people don't like referring to them as result.current[0] and result.current[1] in their tests. I can sympathise with that.

    Another thing many people don't realise is that the value of result.current is whatever you return from the renderHook callback so you can easily get nicely named value by changing your renderHook call to something like:

    // ...
        const { result } = renderHook(() => {
          const [get, set] = React.useState({
            data1: 0,
            data2: 0,
            data3: 0
          })
          return { get, set }
        });
    
        act(() => {
          result.current.set({ data1: 1, data2: 2, data3: 3 });
        });
    
        expect(result.current.get).toStrictEqual({
          data1: 1,
          data2: 2,
          data3: 3
        });
      });
    // ...
    

    Anyway, hope that clears it up and happy testing!