简体   繁体   English

反应 memory 泄漏 - 通过 function 在上下文提供者中更新 state 时传递给提供者的孩子

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

After some debugging I understand the issue and I know roughly why it's happening, so I will show as much code as I can.经过一些调试后,我理解了这个问题,并且我大致知道它发生的原因,所以我将尽可能多地显示代码。

The Error错误

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)

So to give you context, the React structure looks a bit like so所以为了给你上下文,React 结构看起来有点像这样

Simplified version简化版

App.jsx > Router > GlobalLayoutProvider > Route > Page App.jsx > 路由器 > GlobalLayoutProvider > 路由 > 页面

Within the GlobalLayoutProvider I pass six functions down via the new react context, the code looks like so.在 GlobalLayoutProvider 中,我通过新的反应上下文向下传递了六个函数,代码如下所示。 All these functions provide is the ability to modify the state of the layout component, so that if child elements have more complex requirements they can send the information up after performing fetchs etc or they could on mount set the values of the layout.所有这些功能都提供了修改布局组件的 state 的能力,这样如果子元素有更复杂的要求,他们可以在执行获取等之后发送信息,或者他们可以在挂载时设置布局的值。

GlobalLayoutRedux.jsx 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;

Inside the Page.jsx we have a componentDidMount and a componentDidUpdate.在 Page.jsx 中,我们有一个 componentDidMount 和一个 componentDidUpdate。 The issue seems to stem from calling the parent function and setting the state pretty much at any point prior to updating the state of the child component.该问题似乎源于在更新子组件的 state 之前的任何时候调用父 function 并设置 state 。

Page.jsx页面.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>)
  }
}

What's odd the memory leak will disappear if I add this suggestion I seen on stack overflow suggesting to track the state of the components interacting with the higher-level component.如果我添加这个我在堆栈溢出中看到的建议跟踪与更高级别组件交互的组件的 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>)
  }
}

Personally, I don't see this as a solution if I was building my own thing I wouldn't be too fussed but I can't ask an entire company to start adding this addition everywhere.就个人而言,我不认为这是一个解决方案,如果我正在构建自己的东西我不会太大惊小怪,但我不能要求整个公司开始在任何地方添加这个添加。

My gut is telling me that because the component is firing up an object to configure the state of the parent which is for a fraction of a second unmounting as the component did mount is still processing due to the async network fetch when that is returned it is saving to the state before the parent has managed to render the function call state change.我的直觉告诉我,因为该组件正在启动 object 来配置父级的 state,这是在几分之一秒的时间内卸载,因为组件确实挂载仍在处理中,因为在返回时异步网络获取它是在父母设法呈现 function 调用 state 更改之前保存到 state。

What was odd if I pass the callbacks into the parent and call them once the setState has been actioned the issue is resolved like so如果我将回调传递给父级并在 setState 被执行后调用它们,那么问题就这样解决了,这很奇怪

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

but again this causes havoc on the testing side as you are abstracting the componentDidmount functionality out and calling it after a set state is actioned elsewhere.但这再次在测试方面造成严重破坏,因为您将 componentDidmount 功能抽象出来并在其他地方执行一组 state 后调用它。

I have tried adapting what I have to Redux but I had the exact same result everything from a viewing perspective in the browser was fine but still getting the same memory leak using the Redux calls to try to populate all the data from top to bottom.我已经尝试将我所拥有的内容调整为 Redux,但我得到了完全相同的结果,从浏览器中的查看角度来看,一切都很好,但仍然得到相同的 memory 泄漏,使用 Z19D8912E22112EB355C200CC33512D 将所有数据从顶部调用填充到底部。

I can't think of any way of handling this gracefully where we don't need to ask the company to add that fix everywhere.我想不出任何方式来优雅地处理这个问题,我们不需要要求公司在任何地方添加这个修复程序。

So to save people time and effort it turns out our memory leak was actually being cause by a bad set state in the routers of our application.因此,为了节省人们的时间和精力,事实证明我们的 memory 泄漏实际上是由我们应用程序的路由器中的错误设置 state 引起的。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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