React内存泄漏-当通过传递给提供者的子函数更新上下文提供者中的状态时



经过一些调试后,我理解了这个问题,并且我大致知道它发生的原因,因此我将尽可能多地展示代码。

的错误
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结构看起来像这样

简化版

应用程序。jsx祝辞路由器比;GlobalLayoutProvider祝辞路线比;页

在GlobalLayoutProvider中,我通过新的react上下文传递了六个函数,代码如下所示。所有这些函数提供的是修改布局组件状态的能力,这样,如果子元素有更复杂的需求,他们可以在执行fetch等操作后发送信息,或者他们可以在mount上设置布局的值。

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有一个componentDidMount和一个componentdiduupdate。这个问题似乎源于调用父函数并在更新子组件的状态之前的任何时候设置状态。

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

奇怪的是,如果我添加我在堆栈溢出中看到的建议,建议跟踪与更高级别组件交互的组件的状态,内存泄漏将消失。

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

就我个人而言,我不认为这是一个解决方案,如果我是建立自己的东西,我不会太大惊小怪,但我不能要求整个公司开始到处添加这个附加功能。

我的直觉告诉我,因为组件正在启动一个对象来配置父组件的状态,这是一小部分秒的卸载,因为组件确实挂载仍在处理,由于异步网络获取返回时,它正在保存到父组件已经设法渲染函数调用状态更改之前的状态。

如果我将回调传递给父类并在setState被执行后调用它们,那么问题就会像这样解决

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

但是这又会在测试端造成混乱,因为您将componentDidmount功能抽象出来,并在其他地方执行set状态后调用它。

我已经尝试适应什么我有Redux,但我有完全相同的结果,一切从浏览器的视角来看是好的,但仍然得到相同的内存泄漏使用Redux调用试图填充所有的数据从上到下。

我想不出有什么方法可以优雅地处理这个问题,我们不需要要求公司在所有地方添加这个修复。

所以为了节省人们的时间和精力,我们的内存泄漏实际上是由应用程序路由器的坏设置状态引起的。

最新更新