title: Testing Asynchronous Code (useEffect + fetch) In React Components
body: <p>I'm trying to figure out how to test components that update state using useEffect to make an API call to get data. There are several things I think are important to know before I can talk anymore, and that is the files/packages I'm using.</p>
  <p>First, I have a main component called <code>App.tsx</code>, inside <code>App.tsx</code>, inside of useEffect, I make a fetch call to an external API to fetch an array of songs by Queen. I also render out a <code>&lt;Song /&gt;</code> component using <code>.map</code> to iterate over each song and <code>.filter</code> to filter songs on UI based on text input. I'm using a custom hook. Here is the code I have for that component and its custom hook.</p>
  <pre><code>// App.tsx
  
  type ISong = {
    id: number;
    title: string;
    lyrics: string;
    album: string;
  };
  
  export default function App() {
    const { songs, songError } = useSongs();
    const { formData, handleFilterSongs } = useForm();
  
    return (
      &lt;Paper&gt;
        &lt;h1&gt;Queen Songs&lt;/h1&gt;
        &lt;FilterSongs handleFilterSongs={handleFilterSongs} /&gt;
        &lt;section&gt;
          {songError ? (
            &lt;p&gt;Error loading songs...&lt;/p&gt;
          ) : !songs ? (
            &lt;&gt;
              &lt;p data-testid=&quot;loadingText&quot;&gt;Loading...&lt;/p&gt;
              &lt;Loader /&gt;
            &lt;/&gt;
          ) : (
            &lt;Grid container&gt;
              {songs
                .filter(
                  (song: ISong) =&gt;
                    song.title
                      .toLowerCase()
                      .includes(formData.filter.toLowerCase()) ||
                    song.album
                      .toLowerCase()
                      .includes(formData.filter.toLowerCase()) ||
                    song.lyrics
                      .toLowerCase()
                      .split(&quot; &quot;)
                      .join(&quot; &quot;)
                      .includes(formData.filter.toLowerCase())
                )
                .map((song: ISong) =&gt; (
                  &lt;Grid key={song.id} item&gt;
                    &lt;Song song={song} /&gt;
                  &lt;/Grid&gt;
                ))}
            &lt;/Grid&gt;
          )}
        &lt;/section&gt;
      &lt;/Paper&gt;
    );
  }
  
  </code></pre>
  <pre><code>// useSongs.tsx
  
  type ISongs = {
      id: number;
      title: string;
      lyrics: string;
      album: string;
    }[];
    
    type IError = {
      message: string;
    };
  
  export default function useSongs() {
      const [songs, setSongs] = useState&lt;ISongs | null&gt;(null);
      const [songError, setSongError] = useState&lt;IError | null&gt;(null);
  
      useEffect(() =&gt; {
        fetch(&quot;https://queen-songs.herokuapp.com/songs&quot;)
          .then(res =&gt; res.json())
          .then(songs =&gt; setSongs(songs))
          .catch(err =&gt; setSongError(err));
        }, []);
      
        return {songs, songError}
  }
  
  </code></pre>
  <p>Next up is my App.test.tsx file. I am using <code>react-testing-library</code> and <code>jest-dom/extend-expect</code> for my testing coverage. Here is my testing file code. I've been watching a youtube tutorial on the matter and I've read a bunch of articles, but I still can't figure this out.</p>
  <pre><code>// App.test.tsx
  
  import * as rctl from &quot;@testing-library/react&quot;;
  import &quot;@testing-library/jest-dom/extend-expect&quot;;
  import App from &quot;./App&quot;;
  
  // @ts-ignore
  global.fetch = jest.fn(() =&gt;
    Promise.resolve({
      json: () =&gt;
        Promise.resolve({
          value: [{title: &quot;title1&quot;, album: &quot;album1&quot;, lyrics: &quot;asdf&quot;, id: 1}, {title: &quot;title2&quot;, album: &quot;album2&quot;, lyrics: &quot;zxcv&quot;, id: 2}, etc...],
        }),
    })
  );
  
  describe.only(&quot;The App component should&quot;, () =&gt; {
    it(&quot;load songs from an API call after initial render&quot;, async () =&gt; {
      await rctl.act(async () =&gt; {
        await rctl.render(&lt;App /&gt;).debug();
        rctl.screen.debug();
      });
    });
  });
  
  </code></pre>
  <p>This code gives me the following error message</p>
  <pre><code>
   FAIL  src/pages/App/App.test.tsx
    App
      × loads the songs on render (117 ms)
  
    ● App › loads the songs on render
  
      TypeError: Cannot read property 'then' of undefined
  
        17 |
        18 |     useEffect(() =&gt; {
      &gt; 19 |       fetch(&quot;https://queen-songs.herokuapp.com/songs&quot;)
           |       ^
        20 |         .then(res =&gt; res.json())
        21 |         .then(songs =&gt; {
        22 |           setSongs(songs)
  
        at src/pages/App/useSongs.ts:19:7
        at invokePassiveEffectCreate (node_modules/react-dom/cjs/react-dom.development.js:23487:20)
        at HTMLUnknownElement.callCallback (node_modules/react-dom/cjs/react-dom.development.js:3945:14)     
        at HTMLUnknownElement.callTheUserObjectsOperation (node_modules/jsdom/lib/jsdom/living/generated/EventListener.js:26:30)
        at innerInvokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:338:25) 
        at invokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:274:3)       
        at HTMLUnknownElementImpl._dispatch (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:221:9)
        at HTMLUnknownElementImpl.dispatchEvent (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:94:17)
        at HTMLUnknownElement.dispatchEvent (node_modules/jsdom/lib/jsdom/living/generated/EventTarget.js:231:34)
        at Object.invokeGuardedCallbackDev (node_modules/react-dom/cjs/react-dom.development.js:3994:16)     
        at invokeGuardedCallback (node_modules/react-dom/cjs/react-dom.development.js:4056:31)
        at flushPassiveEffectsImpl (node_modules/react-dom/cjs/react-dom.development.js:23574:9)
        at unstable_runWithPriority (node_modules/scheduler/cjs/scheduler.development.js:468:12)
        at runWithPriority$1 (node_modules/react-dom/cjs/react-dom.development.js:11276:10)
        at flushPassiveEffects (node_modules/react-dom/cjs/react-dom.development.js:23447:14)
        at Object.&lt;anonymous&gt;.flushWork (node_modules/react-dom/cjs/react-dom-test-utils.development.js:992:10)
        at flushWorkAndMicroTasks (node_modules/react-dom/cjs/react-dom-test-utils.development.js:1001:5)    
        at node_modules/react-dom/cjs/react-dom-test-utils.development.js:1080:11
  
    console.log
      &lt;body&gt;
        &lt;div&gt;
          &lt;div
            class=&quot;MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded&quot;
            style=&quot;text-align: center; overflow: hidden; min-height: 100vh;&quot;
          &gt;
            &lt;h1&gt;
              Queen Songs
            &lt;/h1&gt;
            &lt;div
              style=&quot;display: flex; flex-flow: column; justify-content: center; text-align: center;&quot;
            &gt;
              &lt;input
                data-testid=&quot;input&quot;
                id=&quot;filter&quot;
                name=&quot;filter&quot;
                placeholder=&quot;Search by title, album name, or lyrics here...&quot;
                style=&quot;width: 18.75rem; height: 1.875rem; align-self: center; text-align: center; font-style: italic;&quot;
                type=&quot;text&quot;
              /&gt;
            &lt;/div&gt;
            &lt;section
              style=&quot;display: flex; flex-flow: row wrap; justify-content: center; align-items: center;&quot;      
            &gt;
              &lt;p
                data-testid=&quot;loadingText&quot;
              &gt;
                Loading...
              &lt;/p&gt;
              &lt;div
                class=&quot;line-container&quot;
              &gt;
                &lt;div
                  class=&quot;line&quot;
                  data-testid=&quot;loader-line&quot;
                /&gt;
              &lt;/div&gt;
            &lt;/section&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/body&gt;
  
        at Object.debug (node_modules/@testing-library/react/dist/pure.js:107:13)
  
    console.log
      &lt;body&gt;
        &lt;div&gt;
          &lt;div
            class=&quot;MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded&quot;
            style=&quot;text-align: center; overflow: hidden; min-height: 100vh;&quot;
          &gt;
            &lt;h1&gt;
              Queen Songs
            &lt;/h1&gt;
            &lt;div
              style=&quot;display: flex; flex-flow: column; justify-content: center; text-align: center;&quot;
            &gt;
              &lt;input
                data-testid=&quot;input&quot;
                id=&quot;filter&quot;
                name=&quot;filter&quot;
                placeholder=&quot;Search by title, album name, or lyrics here...&quot;
                style=&quot;width: 18.75rem; height: 1.875rem; align-self: center; text-align: center; font-style: italic;&quot;
                type=&quot;text&quot;
              /&gt;
            &lt;/div&gt;
            &lt;section
              style=&quot;display: flex; flex-flow: row wrap; justify-content: center; align-items: center;&quot;      
            &gt;
              &lt;p
                data-testid=&quot;loadingText&quot;
              &gt;
                Loading...
              &lt;/p&gt;
              &lt;div
                class=&quot;line-container&quot;
              &gt;
                &lt;div
                  class=&quot;line&quot;
                  data-testid=&quot;loader-line&quot;
                /&gt;
              &lt;/div&gt;
            &lt;/section&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/body&gt;
  
        at logDOM (node_modules/@testing-library/dom/dist/pretty-dom.js:82:13)
  
    console.error
      Error: Uncaught [TypeError: Cannot read property 'then' of undefined]
          at reportException (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\helpers\runtime-script-errors.js:62:24)
          at innerInvokeEventListeners (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:341:9)
          at invokeEventListeners (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:274:3)
          at HTMLUnknownElementImpl._dispatch (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:221:9)
          at HTMLUnknownElementImpl.dispatchEvent (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:94:17)
          at HTMLUnknownElement.dispatchEvent (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\generated\EventTarget.js:231:34)
          at Object.invokeGuardedCallbackDev (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom.development.js:3994:16)
          at invokeGuardedCallback (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom.development.js:4056:31)
          at flushPassiveEffectsImpl (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom.development.js:23574:9)
          at unstable_runWithPriority (C:\Users\brian\Code\cra-queen-api-fe\node_modules\scheduler\cjs\scheduler.development.js:468:12) TypeError: Cannot read property 'then' of undefined
          at C:\Users\brian\Code\cra-queen-api-fe\src\pages\App\useSongs.ts:19:7
          at invokePassiveEffectCreate (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom.development.js:23487:20)
          at HTMLUnknownElement.callCallback (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom.development.js:3945:14)
          at HTMLUnknownElement.callTheUserObjectsOperation (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\generated\EventListener.js:26:30)
          at innerInvokeEventListeners (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:338:25)
          at invokeEventListeners (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:274:3)
          at HTMLUnknownElementImpl._dispatch (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:221:9)
          at HTMLUnknownElementImpl.dispatchEvent (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:94:17)
          at HTMLUnknownElement.dispatchEvent (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\generated\EventTarget.js:231:34)
          at Object.invokeGuardedCallbackDev (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom.development.js:3994:16)
          at invokeGuardedCallback (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom.development.js:4056:31)
          at flushPassiveEffectsImpl (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom.development.js:23574:9)
          at unstable_runWithPriority (C:\Users\brian\Code\cra-queen-api-fe\node_modules\scheduler\cjs\scheduler.development.js:468:12)
          at runWithPriority$1 (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom.development.js:11276:10)
          at flushPassiveEffects (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom.development.js:23447:14)
          at Object.&lt;anonymous&gt;.flushWork (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom-test-utils.development.js:992:10)
          at flushWorkAndMicroTasks (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom-test-utils.development.js:1001:5)
          at C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom-test-utils.development.js:1080:11
          at processTicksAndRejections (node:internal/process/task_queues:94:5)
  
        at VirtualConsole.&lt;anonymous&gt; (node_modules/jsdom/lib/jsdom/virtual-console.js:29:45)
        at reportException (node_modules/jsdom/lib/jsdom/living/helpers/runtime-script-errors.js:66:28)      
        at innerInvokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:341:9)  
        at invokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:274:3)       
        at HTMLUnknownElementImpl._dispatch (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:221:9)
        at HTMLUnknownElementImpl.dispatchEvent (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:94:17)
  
    console.error
      The above error occurred in the &lt;App&gt; component:
      
          at App (C:\Users\brian\Code\cra-queen-api-fe\src\pages\App\App.tsx:18:32)
      
      Consider adding an error boundary to your tree to customize error handling behavior.
      Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.
  
        at logCapturedError (node_modules/react-dom/cjs/react-dom.development.js:20085:23)
        at update.callback (node_modules/react-dom/cjs/react-dom.development.js:20118:5)
        at callCallback (node_modules/react-dom/cjs/react-dom.development.js:12318:12)
        at commitUpdateQueue (node_modules/react-dom/cjs/react-dom.development.js:12339:9)
        at commitLifeCycles (node_modules/react-dom/cjs/react-dom.development.js:20736:11)
        at commitLayoutEffects (node_modules/react-dom/cjs/react-dom.development.js:23426:7)
        at HTMLUnknownElement.callCallback (node_modules/react-dom/cjs/react-dom.development.js:3945:14)     
  
  Test Suites: 1 failed, 1 total
  Tests:       1 failed, 1 total
  Snapshots:   0 total
  Time:        2.59 s, estimated 3 s
  Ran all test suites related to changed files.
  
  </code></pre>
  <p>I honestly am completely lost here, and I have no idea what to do next. My usual problem-solving skills are not helping, so I figured I'd turn to SO for some help. Thank you for reading through all of this and for any help you may be able to provide.</p>
  <p>Edit: I stripped the code of most of the CSS in the snippets to make it slightly more readable, so that is why the screen.debug() log includes some CSS and the code doesn't.</p>
  <p>Edit: I changed the <code>useEffect</code> method to use async/await and now my tests work, but I still have the same output as before. Here is the updated <code>useEffect</code> and the code output.</p>
  <pre><code>// Updated useSongs.tsx
  
  export default function useSongs() {
      const [songs, setSongs] = useState&lt;ISongs | null&gt;(null);
      const [songError, setSongError] = useState&lt;IError | null&gt;(null);
  
      useEffect(() =&gt; {
        (async() =&gt; {
          try {
            const fetchSongs = await fetch(&quot;https://queen-songs.herokuapp.com/songs&quot;);
            const data = await fetchSongs.json();
            setSongs(data);
          } catch (error) {
            setSongError(error);
          }
        })()
      }, []);
      
        return {songs, songError}
  }
  
  </code></pre>
  <pre><code> // Updated testOutput
  
   PASS  src/pages/App/App.test.tsx
    App
      √ loads the songs on render (52 ms)
  
    console.log
      &lt;body&gt;
        &lt;div&gt;
          &lt;div
            class=&quot;MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded&quot;
            style=&quot;text-align: center; overflow: hidden; min-height: 100vh;&quot;
          &gt;
            &lt;h1&gt;
              Queen Songs
            &lt;/h1&gt;
            &lt;section
              style=&quot;display: flex; flex-flow: row wrap; justify-content: center; align-items: center;&quot;       
            &gt;
              &lt;p
                data-testid=&quot;loadingText&quot;
              &gt;
                Loading...
              &lt;/p&gt;
              &lt;div
                class=&quot;line-container&quot;
              &gt;
                &lt;div
                  class=&quot;line&quot;
                  data-testid=&quot;loader-line&quot;
                /&gt;
              &lt;/div&gt;
            &lt;/section&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/body&gt;
  
        at logDOM (node_modules/@testing-library/dom/dist/pretty-dom.js:82:13)
  
  Test Suites: 1 passed, 1 total
  Tests:       1 passed, 1 total
  Snapshots:   0 total
  Time:        0.878 s, estimated 1 s
  Ran all test suites related to changed files.
  
  </code></pre>
  <p>I want the test to show the HTML <em>after</em> the useEffect has run and the state has updated, the loading text should be gone.</p>