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

I'm curious if there's a method to ensure that the content within a mat-card stays responsive

So I'm working with this mat-card: <mat-card> <mat-card-content> <div *ngFor="let siteSource of siteSources | paginate: { itemsPerPage: 5, currentPage: page};"> <site-details [site]='siteSource'></s ...

Toggle the visibility of a dropdown menu based on the checkbox being checked or unchecked

One challenge I am facing involves displaying and hiding DropDown/Select fields based on the state of a Checkbox. When the checkbox is checked, the Dropdown should be visible, and when unchecked, it should hide. Below is the code snippet for this component ...

Using a function from one class within another class by passing it as a prop

Below are the methods found in my Search.tsx class. renderSuggestion(suggestion) { <div className="buttons"> <button className="button">View Location</button> <button className="button whitebutton" onClick={this.h ...

What is the best way to display API error messages to the user?

When a user tries to upload a file that is not an image, I need to display the error message returned from a REST API. The JSON response received from the API will look something like this: { "publicError": "Could not be uploaded, it is not an image! ...

Implementing OTP input using Material UI textfield

Is it possible to create an OTP input using the textfield component of material UI in a React TypeScript project? I've seen examples where people have implemented this with regular input fields, but I'm specifically interested in utilizing the te ...

What is the best way to access all the attributes (excluding methods) of an object in a class instance?

My goal is to generate a new object type that inherits all the properties from an existing class instance. In other words, I am looking for a way to transform a class instance into a plain object. For example, consider the following scenario: ...

Encountering the error "Object potentially undefined" while attempting to activate Cloud Function during document creation

My goal is to automatically create a new authenticated user whenever a new document is added to the "users" collection. The necessary user details will be extracted from the fields "email" and "phone" in the new document. The challenge I am facing is that ...

Encountering Difficulties Setting Variable in Vue

Recently delving into the world of VueJS & Tailwind, I am faced with a steep learning curve as I navigate npm for the first time. Within my code below lies a mixture of Tailwind and Headless UI that is almost complete, except for one nagging error message ...

SWR NextJS error remains unhandled despite being thrown

I'm struggling to manage errors thrown in my SWR fetcher. Currently, whenever an error is thrown, my app stops working and I see an "Unhandled Runtime Error". What I've been doing is throwing the error inside the fetcher function. Here's th ...

Ways to enhance a component by incorporating default properties in React/TypeScript

I am looking to enhance a React/TS component (specifically the @mui/x-data-grid DataGrid) by populating the classes prop with my own application classes. Initially, I thought about creating a new component called CustomDataGrid like this: import React fro ...

Sharing data between two Angular 2 component TypeScript files

I'm facing a scenario where I have two components that are not directly related as parent and child, but I need to transfer a value from component A to component B. For example: In src/abc/cde/uij/componentA.ts, there is a variable CustomerId = "sss ...

Manipulating Angular and Typescript to utilize the method's parameter value as a JavaScript object's value

I am currently working with Ionic, Angular, and Typescript, attempting to dynamically set the value of a location based on the parameter passed from a method. Here is the relevant code snippet: async fileWrite(location) { try { const result = a ...

What is the process for initiating a local Lambda edge viewer request?

Is there a way to run aws cloudfront lambda edge functions locally and simulate the event in order to observe the response from one of the four functions? I made modifications to the viewerRequest function of lambdaEdge, but I'm wondering if there is ...

What steps do I need to take in order to activate scrolling in a Modal using Material-UI

Can a Modal be designed to work like a Dialog with the scroll set to 'paper'? I have a large amount of text to show in the Modal, but it exceeds the browser window's size without any scrolling option. ...

Error in JavaScript: A surprise anonymous System.register call occurred

Within Visual Studio 2015, there exists a TypeScript project featuring two distinct TypeScript files: foo.ts export class Foo { bar(): string { return "hello"; } } app.ts /// <reference path="foo.ts"/> import {Foo} from './f ...

Animate in Angular using transform without requiring absolute positioning after the animation is completed

Attempting to incorporate some fancy animations into my project, but running into layout issues when using position: absolute for the animation with transform. export function SlideLeft() { return trigger('slideLeft', [ state('void&a ...

What is the process for defining a type that retrieves all functions from a TypeScript class?

Imagine having a class called Foo class Foo { bar(){ // do something } baz() { // do something } } How can you define a type ExtractMethods that takes a class and returns an interface or type containing the class methods? For example: t ...

What is the best way to click on a particular button without activating every button on the page?

Struggling to create buttons labeled Add and Remove, as all the other buttons get triggered when I click on one. Here's the code snippet in question: function MyFruits() { const fruitsArray = [ 'banana', 'banana', & ...

Retrieve a list of class names associated with a Playwright element

Can anyone suggest the best method to retrieve an array of all class names for an element in Playwright using TypeScript? I've searched for an API but couldn't find one, so I ended up creating the following solution: export const getClassNames = ...

"Creating a Typescript property that is calculated based on other existing properties

I am working on a Typescript project where I have a basic class that includes an `id` and `name` property. I would like to add a third property called `displayText` which combines the values of these two properties. In C#, I know how to achieve this using ...