What is the best approach to testing the React Hook "useEffect" that is used to make an API call with Typescript?

Currently, I am working on writing Jest-enzyme tests for a basic React application using Typescript along with the new React hooks.

The main issue I am facing is with properly simulating the api call made within the useEffect hook.

Within the useEffect, the api call is initiated and updates the state "data" using "setData". The data object is then converted into a table with corresponding table cells.

While attempting to tackle this with a mocked api response and enzyme mount, I keep encountering errors prompting me to use act() for component updates.

I have tried various ways of using act() but have been unsuccessful. I also attempted replacing axios with fetch and utilized enzyme shallow along with react-test-library's render, but none of these solutions seem to work.

The component:

import axios from 'axios'
import React, { useEffect, useState } from 'react';

interface ISUB {
  id: number;
  mediaType: {
    digital: boolean;
    print: boolean;
  };
  monthlyPayment: {
    digital: boolean;
    print: boolean;
  };
  singleIssue: {
    digital: boolean;
    print: boolean;
  };
  subscription: {
    digital: boolean;
    print: boolean;
  };
  title: string;
}

interface IDATA extends Array<ISUB> {}

const initData: IDATA = [];

const SalesPlanTable = () => {
  const [data, setData] = useState(initData);
  useEffect(() => {
    axios
      .get(`/path/to/api`)
      .then(res => {
        setData(res.data.results);
      })
      .catch(error => console.log(error));
  }, []);

  const renderTableRows = () => {
    return data.map((i: ISUB, k: number) => (
      <tr key={k}>
        <td>{i.id}</td>
        <td>
          {i.title}
        </td>
        <td>
          {i.subscription.print}
          {i.mediaType.digital}
        </td>
        <td>
          {i.monthlyPayment.print}
          {i.monthlyPayment.digital}
        </td>
        <td>
          {i.singleIssue.print}
          {i.singleIssue.digital}
        </td>
        <td>
          <button>Submit</button>
        </td>
      </tr>
    ));
  };

  return (
    <table>
      <thead>
        <tr>
          <th>ID</th>
          <th>Name</th>
          <th>MediaType</th>
          <th>MonthlyPayment</th>
          <th>SingleIssue</th>
          <th/>
        </tr>
      </thead>
      <tbody'>{renderTableRows()}</tbody>
    </table>
  );
};

export default SalesPlanTable;

The test:

const response = {
  data: {
    results: [
      {
        id: 249,
        mediaType: {
          digital: true,
          print: true
        },
        monthlyPayment: {
          digital: true,
          print: true
        },
        singleIssue: {
          digital: true,
          print: true
        },
        subscription: {
          digital: true,
          print: true
        },
        title: 'ELLE'
      }
    ]
  }
};

//after describe

it('should render a proper table data', () => {
    const mock = new MockAdapter(axios);
    mock.onGet('/path/to/api').reply(200, response.data);
    act(() => {
      component = mount(<SalesPlanTable />);
    })
    console.log(component.debug())
  });

My expectation was to log the HTML of the table with the rendered table body section. Despite trying different approaches involving async functions and various methods to mock axios, I only end up seeing the table headers or receiving the message: An update to SalesPlanTable inside a test was not wrapped in act(...). After extensive research for a solution with no success, I decided to seek help here.

Answer №1

There are two key issues to address in this situation


Dealing with Asynchronous Calls to setData

setData is being invoked within a Promise callback.

Once a Promise is resolved, any pending callbacks are lined up in the PromiseJobs queue. The tasks waiting in the PromiseJobs queue will execute after the current task finishes and before the next one begins.

In this case, your test completes first before the Promise callback has a chance to run, causing setData to be called after your test has finished.

To resolve this issue, consider utilizing something like setImmediate to delay your assertions until after the PromiseJobs queue has cleared.

Additonally, you may need to call component.update() to re-render the component with the updated state. (This could be necessary since the state change occurs outside of an act block where wrapping it was not feasible.)

The modified test script would resemble the following:

it('should display correct table data', done => {
  const mock = new MockAdapter(axios);
  mock.onGet('/path/to/api').reply(200, response.data);
  const component = mount(<SalesPlanTable />);
  setImmediate(() => {
    component.update();
    console.log(component.debug());
    done();
  });
});

Notification: An update to ... occurred outside of act(...)

This warning emerges when there are state updates happening within a component that fall outside the scope of an act operation.

Changes in state due to asynchronous invocations of setData triggered by a useEffect function are inevitably executed outside of an act context.

An illustrative straightforward test displaying this behavior is outlined below:

import React, { useState, useEffect } from 'react';
import { mount } from 'enzyme';

const SimpleComponent = () => {
  const [data, setData] = useState('initial');

  useEffect(() => {
    setImmediate(() => setData('updated'));
  }, []);

  return (<div>{data}</div>);
};

test('SimpleComponent', done => {
  const wrapper = mount(<SimpleComponent/>);
  setImmediate(done);
});

While delving further into this matter, I chanced upon enzyme issue #2073 recently opened to discuss this very scenario.

I included the above test example within a comment to aid the enzyme developers in addressing this concern.

Answer №2

Solution

This method is effective in both achieving the desired outcome and resolving the issue of the warning message regarding test was not wrapped in act(...).

const waitForComponentToPaint = async (wrapper) => {
   await act(async () => {
     await new Promise(resolve => setTimeout(resolve, 0));
     wrapper.update();
   });
};

Usage:

it('should do something', () => {
    const wrapper  = mount(<MyComponent ... />);
    await waitForComponentToPaint(wrapper);
    expect(wrapper).toBlah...
})

Special Thanks

Credit for this workaround goes to edpark11 as mentioned in the issue referenced in a response by @Brian_Adams which can be found here.

For the original post, visit: https://github.com/enzymejs/enzyme/issues/2073#issuecomment-565736674

I have rephrased some parts of the post here for archival purposes.

Answer №3

It's not recommended to mock the library used for fetch requests. If you decide to switch from axios to fetch or isomorphic-unfetch, all mocks in your test suite will need to be replaced. It's more effective to align your tests with server contracts rather than relying on mocks.

Consider using a server stub library like msw or nock along with React Testing Library (RTL) which provides tools for managing asynchronous operations during React's lifecycle.

This is how I would rewrite the test based on your example:

RTL + Nock

/* SalesPlanTable.jsx */

import axios from 'axios';
import React, { useEffect, useState } from 'react';

interface ISUB {
  id: number;
  mediaType: {
    digital: boolean;
    print: boolean;
  };
  monthlyPayment: {
    digital: boolean;
    print: boolean;
  };
  singleIssue: {
    digital: boolean;
    print: boolean;
  };
  subscription: {
    digital: boolean;
    print: boolean;
  };
  title: string;
}

interface IDATA extends Array<{ISUB}> {}

const initData: IDATA = [];

const SalesPlanTable = () => {
  const [data, setData] = useState(initData);
  const [status, setStatus] = useState('loading');

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await axios.get('/path/to/api');
        setData(response.data.results);
        setStatus('ready');
      } catch (error) {
        console.log(error);
        setStatus('error');
      }
    };

    fetchData();
  }, []);

  const renderTableRows = () => {
    return data.map((i: ISUB, k: number) => (
      <tr key={k}>
        <td>{i.id}</td>
        <td>{i.title}</td>
        <td>
          {i.subscription.print}
          {i.mediaType.digital}
        </td>
        <td>
          {i.monthlyPayment.print}
          {i.monthlyPayment.digital}
        </td>
        <td>
          {i.singleIssue.print}
          {i.singleIssue.digital}
        </td>
        <td>
          <button>Submit</button>
        </td>
      </tr>
    ));
  };

  if (status === 'loading') {
    return <div>Loading...</div>;
  }

  if (status === 'error') {
    return <div>Error occurred while fetching data.</div>;
  }

  return (
    <table>
      <thead>
        <tr>
          <th>ID</th>
          <th>Name</th>
          <th>MediaType</th>
          <th>MonthlyPayment</th>
          <th>SingleIssue</th>
          <th />
        </tr>
      </thead>
      <tbody>{renderTableRows()}</tbody>
    </table>
  );
};

export default SalesPlanTable;
/* SalesPlanTable.test.jsx */

import { render, screen } from '@testing-library/react';
import nock from 'nock';

import SalesPlanTable from './SalesPlanTable';

/**
 * @NOTE: This should probably go into a `__fixtures__` folder.
 */
const response = {
  data: {
    results: [
      {
        id: 249,
        mediaType: {
          digital: true,
          print: true,
        },
        monthlyPayment: {
          digital: true,
          print: true,
        },
        singleIssue: {
          digital: true,
          print: true,
        },
        subscription: {
          digital: true,
          print: true,
        },
        title: 'ELLE',
      },
    ],
  },
};

describe('<SalesPlanTable />', () => {
  it('displays the title', async () => {
    const scope = nock('http://localhost')
      .get('/path/to/api')
      .reply(200, response.data);

    render(<SalesPlanTable />);
  
    // Wait for the async task to complete
    await waitFor(() => {
      expect(screen.getByText('Loading...')).not.toBeInTheDocument();
    });
  
    // Test rendering
    expect(screen.getByText('ELLE')).toBeInTheDocument();
    expect(scope.isDone()).toBeTruthy();
  });
});

Enzyme + Nock

/* SalesPlanTable.jsx */

import React from 'react';
import { mount } from 'enzyme';
import nock from 'nock';

import SalesPlanTable from './SalesPlanTable';

const response = {
  data: {
    results: [
      {
        id: 249,
        mediaType: {
          digital: true,
          print: true,
        },
        monthlyPayment: {
          digital: true,
          print: true,
        },
        singleIssue: {
          digital: true,
          print: true,
        },
        subscription: {
          digital: true,
          print: true,
        },
        title: 'ELLE',
      },
    ],
  },
};

describe('<SalesPlanTable />', () => {
  it('displays the title', async () => {
    nock('http://localhost')
      .get('/path/to/api')
      .reply(200, response.data);

    const component = mount(<SalesPlanTable />);
    
    // Wait for API call to complete
    await new Promise((resolve) => setTimeout(resolve)); 
    component.update();

    expect(component.find('td').at(1).text()).toBe('ELLE');
    expect(scope.isDone()).toBeTruthy();
  });
});

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Learn the method of conditionally resetting the state count using React hooks

When a user submits the form, the handleSubmit function runs to count the number of li elements with class names containing "show" within the search results div. It adds up these counts using setStoreCount(prevCount => prevCount + 1) and updates the UI ...

issue with Angular: Unable to set both minimum and maximum values in the same input field for date type

I have been attempting to apply minimum and maximum values to an Angular code snippet, here is what I have: <input type="date" class="form-control" style="width: 30%" [disabled]="!dateSent" min="{{dateSent|date:&apo ...

Utilize an alias to define the SCSS path in an Angular-CLI library project

I am in the process of developing a library project using angular-cli. I have been following the guidelines outlined in the angular documentation. This has resulted in the creation of two main folders: one is located at ./root/src/app, where I can showcase ...

How can we implement type guarding for a generic class in TypeScript?

Implementing a generic class in TypeScript that can return different types based on its constructor parameter. type Type = 'foo' | 'bar'; interface Res { 'foo': {foo: number}; 'bar': {bar: string}; } class ...

Error: unable to locate module that was imported from

Every time I try to run the project using this command, an error pops up: > npm run build npm info using <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="c7a9b7aa87fee9f1e9f0">[email protected]</a> npm info using ...

The KeyConditionExpression is invalid due to the use of multiple attribute names within a single condition

I am trying to query a DynamoDB table using GraphQL TableName: "JobInfo", IndexName: "tableauGSI", KeyConditionExpression: "tableauGSI_Tableau = tableau AND #D BETWEEN :startDate AND :endDate", ExpressionAttributeNames: { "#D": "date" }, ...

Obtaining a customized variation of a class identified by a decorator

I am working with a class that is defined as follows: class Person { @OneToOne() pet: Animal; } Is there a method to obtain a transformed type that appears like this? (Including {propertyKey}Id: string to properties through the use of a decorator) ...

Using TypeScript, let's take a closer look at an example of Angular

I am trying to replicate the chips example found at this link (https://material.angularjs.org/latest/#/demo/material.components.chips) using TypeScript. I have just started learning TypeScript this week and I am having some difficulties translating this co ...

Issue arises when TypeScript attempts to verify the presence of an array element

I am facing an issue with the following array declaration: // Using const as I require it to be a specific type elsewhere const fruits = ["strawberry", "banana", "orange", "grapefruit"] as const; When attempting to ...

Vue: Simple ways to retrieve state data in MutationAction

I'm having trouble accessing the state inside @MutationAction Here is the setup I am using: Nuxt.js v2.13.3 "vuex-module-decorators": "^0.17.0" import { Module, VuexModule, MutationAction } from 'vuex-module-decorators' ...

Create TypeScript declaration files dynamically within the application's memory

Is there a way to programmatically generate declaration files using TypeScript? I know we can use tsc --declaration --emitDeclarationOnly --outFile index.d.ts, but I'm not sure how to do it in code. For example: import ts from 'typescript' c ...

Learn how to easily copy all the text from a grid in React JS by utilizing the "Copy All" icon from MUI

Hey everyone, I've included "Copy All" material UI icons in my grid. The grid consists of 2 columns and 2 rows. I want to be able to copy all the values in the columns and rows when the copy all icon is clicked. Code: const items=[{ id:1, name:' ...

What is the correct way to pass parameters when using the setState() function in React hooks?

I am working on a project where I have a list of country names. When a user clicks on one of the countries, I want to update the state with the name of the newly selected country. This state change will then trigger other changes in a useEffect(). The stat ...

Replace a portion of text with a RxJS countdown timer

I am currently working on integrating a countdown timer using rxjs in my angular 12 project. Here is what I have in my typescript file: let timeLeft$ = interval(1000).pipe( map(x => this.calcTimeDiff(orderCutOffTime)), shareReplay(1) ); The calcTim ...

Creating and updating a TypeScript definition file for my React component library built with TypeScript

As I work on developing a React library using TypeScript, it is important to me that consumers of the library have access to a TypeScript definition file. How can I ensure that the TypeScript definition file always accurately reflects and matches the Java ...

Currently, I'm harnessing the power of TypeScript and React to identify and capture a click event on a dynamically generated element within my document

Is there a way to detect a click on the <p> tag with the ID of "rightDisplayBtn"? I've tried using an onclick function and event listener, but neither seem to be working as expected. function addDetails() { hideModal(); addBook ...

How to Retrieve an Array from a Promise Using Angular 4 and Typescript

I am encountering difficulties when trying to store data from a returned promise. To verify that the desired value has been returned, I log it in this manner: private fetchData() { this._movieFranchiseService.getHighestGrossingFilmFranchises() ...

Provider not found: ConnectionBackend – NullInjectorError

I encountered the following error while attempting to load the webpage. Despite trying various suggestions from other sources, I have been unable to find a solution. Below the error stack lies my code. core.js:7187 ERROR Error: Uncaught (in promise): Null ...

What strategies can I employ to create test cases for the untested code within useEffect and dispatch functions?

Just beginning my journey in writing tests, so this is all new to me. Executing the command below: $ yarn test:coverage This is the output I get: https://i.stack.imgur.com/cqAwo.png I need to retrieve a list of Models through dispatch when the dropdo ...

SonarLint versus SonarTS: A Comparison of Code Quality Tools

I'm feeling pretty lost when it comes to understanding the difference between SonarLint and SonarTS. I've been using SonarLint in Visual Studio, but now my client wants me to switch to the SonarTS plugin. SonarLint is for analyzing overall pr ...