簡體   English   中英

反應 memory 泄漏 - 通過 function 在上下文提供者中更新 state 時傳遞給提供者的孩子

[英]React memory leak - when updating state in context provider via a function passed to a child of the provider

經過一些調試后,我理解了這個問題,並且我大致知道它發生的原因,所以我將盡可能多地顯示代碼。

錯誤

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 the componentWillUnmount method.
    in ProductsDisplay (created by ConnectFunction)
    in ConnectFunction (created by Context.Consumer)
    in Route (created by SiteRouter)
    in Switch (created by SiteRouter)
    in SiteRouter (created by ConnectFunction)
    in ConnectFunction (created by TLORouter)
    in Route (created by TLORouter)
    in Switch (created by TLORouter)

所以為了給你上下文,React 結構看起來有點像這樣

簡化版

App.jsx > 路由器 > GlobalLayoutProvider > 路由 > 頁面

在 GlobalLayoutProvider 中,我通過新的反應上下文向下傳遞了六個函數,代碼如下所示。 所有這些功能都提供了修改布局組件的 state 的能力,這樣如果子元素有更復雜的要求,他們可以在執行獲取等之后發送信息,或者他們可以在掛載時設置布局的值。

GlobalLayoutRedux.jsx

class GlobalLayoutProvider extends React.Component {
  constructor(props) {
    super(props);
    this.state = { routeConfig: null };
    this.getRouteData = this.getRouteData.bind(this);
    this.setLoaderOptions = this.setLoaderOptions.bind(this);
  }

  componentDidMount() {
    this.getRouteData();
  }

  componentDidUpdate(prevProps) {
    const { urlParams, user, layoutSettings } = this.props;

    if (
      urlParams.pathname !== prevProps.urlParams.pathname
      || user.permissions !== prevProps.user.permissions
    ) {
      this.getRouteData();
    }
  }

  getRouteData() {
    const { user, urlParams } = this.props;
    const { tlo, site, pathname } = urlParams;

    this.setState({
      routeConfig: pageConfigs().find(
        (c) => c.pageContext(tlo, site, user) === pathname,
      ),
    });
  }

  setLoaderOptions(data) {
    this.setState((prevState) => ({
      routeConfig: {
        ...prevState.routeConfig,
        loader: {
          display: data?.display || initialState.loader.display,
          message: data?.message || initialState.loader.message,
        },
      },
    }));
  }

  render() {
    const { routeConfig } = this.state;
    const { children, user } = this.props;

    return (
      <GlobalLayoutContext.Provider
        value={{
          setLoaderOptions: this.setLoaderOptions,
        }}
      >
        <PageContainer
          title={routeConfig?.pageContainer?.title}
          breadcrumbs={[routeConfig?.pageContainer?.title]}
        >
          <ActionsBar
            actionsBarProperties={{ actions: routeConfig?.actionBar?.actions }}
            pageTitle={routeConfig?.actionBar?.title}
          />
          <SideNav items={routeConfig?.sideNav?.options} selected={routeConfig?.sideNav?.pageNavKey}>
            <div id={routeConfig?.sideNav?.pageNavKey} className="Content__body page-margin">
              <div id="loader-instance" className={`${routeConfig?.loader?.display ? '' : 'd-none'}`}>
                <Loader message={routeConfig?.loader?.message} />
              </div>
              <div id="children-instance" className={`${routeConfig?.loader?.display ? 'd-none' : ''}`}>
                {children}
              </div>
            </div>
          </SideNav>
        </PageContainer>
      </GlobalLayoutContext.Provider>
    );
  }
}


export default GlobalLayoutProvider;

在 Page.jsx 中,我們有一個 componentDidMount 和一個 componentDidUpdate。 該問題似乎源於在更新子組件的 state 之前的任何時候調用父 function 並設置 state 。

頁面.jsx

export default class Page extends Component {
  static contextType = GlobalLayoutContext;

  constructor(props) {
    super(props);
    this.state = {
      someState: 'stuff'
    };
  }

  componentDidMount() {
    this.setActionBarButtons();
    this.fetchOrganisationsProducts();
  }

  async componentDidUpdate(prevProps) {
    const { shouldProductsRefresh, selectedOrganisation, permissions } = this.props;

    if (
      selectedOrganisation?.id !== prevProps.selectedOrganisation?.id
      || shouldProductsRefresh !== prevProps.shouldProductsRefresh
    ) {
      await this.fetchOrganisationsProducts();
    }

    if (
      selectedOrganisation?.id !== prevProps.selectedOrganisation?.id
      || shouldProductsRefresh !== prevProps.shouldProductsRefresh
      || permissions !== prevProps.permissions
    ) {
      this.setActionBarButtons();
    }
  }

  setActionBarButtons() {
    const { setActionBarOptions } = this.context;

    const actions = [
      ActionButtons.Custom(
        () => this.setState({ exportTemplateModalIsOpen: true }),
        { title: 'Button', icon: 'button' },
      ),
    ];

    setActionBarOptions({ actions, title: 'Products', display: true });
  }


  async fetchOrganisationsProducts() {
    const { selectedOrganisation } = this.props;
    const { setLoaderOptions } = this.context;
    setLoaderOptions({ display: true, message: 'Loading Products In Organisation' });

    (await productStoreService.getProducts(selectedOrganisation.id))
      .handleError(() => setLoaderOptions({ display: false }))
      .handleOk((products) => {
        this.setState({ products }, () => {
          setLoaderOptions({ display: false });
          products.forEach(this.fetchAdditionalInformation)
        });
      });
  }

  render() {
    return (<p>Something</p>)
  }
}

如果我添加這個我在堆棧溢出中看到的建議跟蹤與更高級別組件交互的組件的 state,那么 memory 泄漏將消失,這有什么奇怪的。

export default class Page extends Component {
  static contextType = GlobalLayoutContext;

  constructor(props) {
    super(props);
    this.state = {
      someState: 'stuff'
    };
  }
        
  // ADDITION HERE
  _isMounted = false;

  componentDidMount() {
    // ADDITION HERE
    this._isMounted = true;
    this.setActionBarButtons();
    this.fetchOrganisationsProducts();
  }

  // ADDITION HERE
  componentWillUnmount() {
    this._isMounted = false;
  }

  async fetchOrganisationsProducts() {
    const { selectedOrganisation } = this.props;
    const { setLoaderOptions } = this.context;
    setLoaderOptions({ display: true, message: 'Loading Products In Organisation' });

    (await productStoreService.getProducts(selectedOrganisation.id))
      .handleError(() => setLoaderOptions({ display: false }))
      .handleOk((products) => {

        // ADDITION HERE
        if (this._isMounted) {
          this.setState({ products }, () => {
            setLoaderOptions({ display: false });
            products.forEach(this.fetchAdditionalInformation)
          });
        }
      });
  }

  render() {
    return (<p>Something</p>)
  }
}

就個人而言,我不認為這是一個解決方案,如果我正在構建自己的東西我不會太大驚小怪,但我不能要求整個公司開始在任何地方添加這個添加。

我的直覺告訴我,因為該組件正在啟動 object 來配置父級的 state,這是在幾分之一秒的時間內卸載,因為組件確實掛載仍在處理中,因為在返回時異步網絡獲取它是在父母設法呈現 function 調用 state 更改之前保存到 state。

如果我將回調傳遞給父級並在 setState 被執行后調用它們,那么問題就這樣解決了,這很奇怪

  setOnMountOptions(data) {
    this.setState((prevState) => ({
      routeConfig: {
        ...prevState.routeConfig,
        ...data?.loader ? { loader: data.loader } : {},
      },
    }), async () => { await data.callbacks(); });
  }

但這再次在測試方面造成嚴重破壞,因為您將 componentDidmount 功能抽象出來並在其他地方執行一組 state 后調用它。

我已經嘗試將我所擁有的內容調整為 Redux,但我得到了完全相同的結果,從瀏覽器中的查看角度來看,一切都很好,但仍然得到相同的 memory 泄漏,使用 Z19D8912E22112EB355C200CC33512D 將所有數據從頂部調用填充到底部。

我想不出任何方式來優雅地處理這個問題,我們不需要要求公司在任何地方添加這個修復程序。

因此,為了節省人們的時間和精力,事實證明我們的 memory 泄漏實際上是由我們應用程序的路由器中的錯誤設置 state 引起的。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM