React 测试库测试仍然出现 "React state update on an unmounted component" 错误

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

我的测试代码出现了很多错误,因此采取了几个步骤来改进。本文帮助:https://binarapps.com/blog/clean-up-request-in-useeffect-react-hook/woodb

我通过引入 AbortController 改进了我怀疑是问题的 useEffect:

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]);

我也这样重构了我的测试代码:

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();
  });
});

注:renderDom功能与之前相同:

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
  };
}

根据 oemera 的问题,这是异步调用:

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

经过这次重构,errors/warnings 已经减少了,但仍然出现了一个:

>   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.

我已经检查了提到的所有代码 AddUsers... 文件,但那里没有 useEffect 个实例。

有什么建议可以消除此警告吗?

当您使用 axios 而不是 fetch 时,中止控制器将无法工作。这就是你在 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>
      )
    }
}

来源: https://gist.github.com/adeelibr/d8f3f8859e2929f3f1adb80992f1dc09

请注意您是如何传递 cancelToken 而不是整个信号的。您还可以通过 axios.CancelToken.source() 获得信号,然后调用 signal.cancelabortController.abort

所以对于你的例子,这应该像这样工作或者至少引导你朝着正确的方向前进:

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]);