简体   繁体   中英

Still getting "React state update on an unmounted component" error with React Testing Library tests

I was getting a plethora of errors with my testing code so have taken several steps to improve things. This article helped: https://binarapps.com/blog/clean-up-request-in-useeffect-react-hook/woodb

I improved the useEffect that I suspected was the problem by introducing AbortController:

useEffect(() => {
  if (companyId && companyId !== -1) {
    const abortController = new AbortController();

    requests.get(`${requests.API_ROOT()}account_management/roles?company_id=${companyId}`)
      .then(response => {
        dispatch({type: UPDATE_ROLES, payload: response.data.roles});
      })
      .catch(error => {
        if (error.response) {
          throw('Error fetching roles: ', error.response.status);
        }
      });

    return () => {
      abortController.abort();
    };
  }
}, [companyId, dispatch]);

I also refactored my test code like this:

it('Should hide modal if Close is pressed', async () => {
  await act(async () => {
    let { getByTestId, getByText, queryByTestId, queryByText } = await renderDom(<AddUsersLauncher />);
    fireEvent.click(queryByText(/^Add Users/i));

    fireEvent.click(getByText(/^Close/i));
    expect(queryByTestId('add-users-modal')).toBeNull();
  });
});

Note: The renderDom function remains the same as before:

const _initialState = {
  session: {
    user: {},
    isLoading: false,
    error: false,
  },
};

function renderDom(component, initialState = _initialState) {
  const store = configureStore(initialState);

  return {
    ...render(
      <Provider store={store}>
        <SessionProvider>
          <ThemeProvider theme={theme}>
            <SystemMessages />
            <CustomComponent />
            {component}
          </ThemeProvider>
        </SessionProvider>
      </Provider>),
    store
  };
}

As per the question from oemera, this is the async call:

export const get = async function(url: string, _options: any) {
  const options = _options || await _default_options();
  return axios.get(url, options);
};

After this refactoring, the errors/warnings have reduced but there's still one appearing:

>   Add Users component tests
>     ✓ Should display the Add Users modal (119ms)
>     ✓ Should cancel w/o confirmation modal if no data is entered (34ms)
>     ✓ Should hide modal if Close is pressed (32ms)
>     ✓ Should hide modal if Close is pressed (29ms)
> 
>   console.error
> node_modules/react-dom/cjs/react-dom.development.js:558
>     Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your
> application. To fix, cancel all subscriptions and asynchronous tasks
> in a useEffect cleanup function.
>         in UsersProvider (at AddUsersModal.js:10)
>         in div (at AddUsersModal.js:9)
>         in AddUsersModal (at AddUsersLauncher.js:16)
> 
> Test Suites: 1 passed, 1 total Tests:       4 passed, 4 total
> Snapshots:   0 total Time:        4.999s Ran all test suites matching
> /AddUsers.test/i.

I've checked all of the code AddUsers... files mentioned but there are no useEffect instances there.

Any advice on what I should do to remove this warning?

When you are using axios instead of fetch the abort controller won't work. This is how you do it in axios:

import React, { Component } from 'react';
import axios from 'axios';

class Example extends Component {

  signal = axios.CancelToken.source();

  state = {
    isLoading: false,
    user: {},
  }

  componentDidMount() {
    this.onLoadUser();
  }

  componentWillUnmount() {
    this.signal.cancel('Api is being canceled');
  }

  onLoadUser = async () => {

    try {
      this.setState({ isLoading: true });
      const response = await axios.get('https://randomuser.me/api/', {
        cancelToken: this.signal.token,
      })

      this.setState({ user: response.data, isLoading: true });

    } catch (err) {

      if (axios.isCancel(err)) {

        console.log('Error: ', err.message); // => prints: Api is being canceled
      }
      else {
        this.setState({ isLoading: false });
      }
    }
   } 
   
    render() {

      return (
        <div>
          <pre>{JSON.stringify(this.state.user, null, 2)}</pre>
        </div>
      )
    }
}

source: https://gist.github.com/adeelibr/d8f3f8859e2929f3f1adb80992f1dc09

Note how you pass in a cancelToken instead of the whole signal. Also you get the signal with axios.CancelToken.source() and you call signal.cancel instead or abortController.abort

So for your example, this should work like this or at least lead you in the right direction:

useEffect(() => {
 if (companyId && companyId !== -1) { 
   const signal = axios.CancelToken.source();
   requests.get(`${requests.API_ROOT()}account_management/roles?company_id=${companyId}`,
   { cancelToken: signal.token})
  .then(response => { 
   dispatch({type: UPDATE_ROLES, 
   payload: response.data.roles}); })
   .catch(error => { 
      if (error.response) { 
         throw('Error fetching roles: '     error.response.status);
    } 
   }); 
   return () => { signal.cancel('Cancelling request...');};
} }, [companyId, dispatch]);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM