繁体   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