Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: avoid throw error when update unmounted component #2223

Merged
merged 11 commits into from
Aug 27, 2021
115 changes: 115 additions & 0 deletions packages/rax/src/__tests__/asyncUpdate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/* @jsx createElement */

import createElement from '../createElement';
import Component from '../vdom/component';
import render from '../render';
import Host from '../vdom/host';
import ServerDriver from 'driver-server';
import { useState, useEffect } from '../hooks';

describe('update unmounted component', () => {
function createNodeElement(tagName) {
return {
nodeType: 1,
tagName: tagName.toUpperCase(),
attributes: {},
style: {},
childNodes: [],
parentNode: null
};
}

beforeEach(function() {
Host.driver = ServerDriver;
jest.useFakeTimers();
});

afterEach(function() {
Host.driver = null;
jest.useRealTimers();
});

it('should warn about class component', () => {
const container = createNodeElement('div');
let destroyChild;

class Child extends Component {
state = {
name: 'hello'
}
componentDidMount() {
setTimeout(() => {
this.setState({
name: 'work'
});
}, 1000);
}
render() {
return <div>{this.state.name}</div>;
}
}

class App extends Component {
state = {
showChild: true
}
componentDidMount() {
destroyChild = () => {
this.setState({
showChild: false
});
};
}
render() {
return (<div>
{ this.state.showChild ? <Child /> : null }
</div>);
}
}

expect(() => {
render(<App />, container);
destroyChild();
jest.runAllTimers();
}).toWarnDev("Warning: Can't perform a Rax 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.", { withoutStack: true });
});

it('should warn about function component', () => {
const container = createNodeElement('div');
let destroyChild;

function Child() {
const [name, setName] = useState('hello');
useEffect(() => {
setTimeout(() => {
setName('world');
}, 1000);
}, []);
return <div>{name}</div>;
}

class App extends Component {
state = {
showChild: true
}
componentDidMount() {
destroyChild = () => {
this.setState({
showChild: false
});
};
}
render() {
return (<div>
{ this.state.showChild ? <Child /> : null }
</div>);
}
}

expect(() => {
render(<App />, container);
destroyChild();
jest.runAllTimers();
}).toWarnDev("Warning: Can't perform a Rax 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.", { withoutStack: true });
});
});
2 changes: 1 addition & 1 deletion packages/rax/src/__tests__/createContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import render from '../render';
import ServerDriver from 'driver-server';
import createContext from '../createContext';
import createRef from '../createRef';
import {useState} from '../hooks';
import { useState } from '../hooks';

describe('createContext', () => {
function createNodeElement(tagName) {
Expand Down
1 change: 0 additions & 1 deletion packages/rax/src/vdom/reactive.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ export default class ReactiveComponent extends Component {
}

__update() {
this[INTERNAL].__isPendingForceUpdate = true;
this.setState(EMPTY_OBJECT);
}

Expand Down
16 changes: 16 additions & 0 deletions packages/rax/src/vdom/updater.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,18 @@ function requestUpdate(component, partialState, callback) {
let internal = component[INTERNAL];

if (!internal) {
if (process.env.NODE_ENV !== 'production') {
// Block other render
Host.__isUpdating = false;
console.error(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

是哪个组件调用的, warning 中能透出不

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

日志可能会很多

"Warning: Can't perform a Rax 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 %s.',
component.__isReactiveComponent
? 'a useEffect cleanup function'
: 'the componentWillUnmount method',
);
}
return;
}

Expand All @@ -117,6 +129,10 @@ function requestUpdate(component, partialState, callback) {

// setState
if (partialState) {
// Function Component should force update
if (component.__isReactiveComponent) {
internal.__isPendingForceUpdate = true;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这段位置调整的原因是?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

将之前 ReactiveComponent _update 方法中会导致报错的 this[INTERNAL]. __isPendingForceUpdate 移到这个地方

}
enqueueState(internal, partialState);
// State pending when request update in componentWillMount and componentWillReceiveProps,
// isPendingState default is false value (false or null) and set to true after componentWillReceiveProps,
Expand Down
2 changes: 1 addition & 1 deletion scripts/jest/toWarnDev.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// https://github.com/facebook/react/blob/master/scripts/jest/matchers/toWarnDev.js
const jestDiff = require('jest-diff'); // eslint-disable-line import/no-extraneous-dependencies
const jestDiff = require('jest-diff').default; // eslint-disable-line import/no-extraneous-dependencies
const util = require('util');
const shouldIgnoreConsoleError = require('./shouldIgnoreConsoleError');

Expand Down