Skip to content

Commit

Permalink
chore: avoid throw error when update unmounted component (#2223)
Browse files Browse the repository at this point in the history
* refactor: rework bundle core logic

* chore: ci

* chore: revert

* chore: lint

* chore: format code

* chore: avoid throw error when update unmounted component

* chore: remove useless comment

* chore: preserve warning

* chore: typo
  • Loading branch information
SoloJiang authored Aug 27, 2021
1 parent ed68977 commit dbf6d88
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 3 deletions.
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(
"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;
}
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

0 comments on commit dbf6d88

Please sign in to comment.