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

add $F as a force property to the component instance #1535

Merged
merged 2 commits into from
Feb 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 214 additions & 0 deletions packages/inferno/__tests__/forceUpdate.spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import { Component, render, rerender } from 'inferno';

describe('forceUpdate', () => {
let container;

beforeEach(function () {
container = document.createElement('div');
document.body.appendChild(container);
});

afterEach(function () {
rerender(); // Flush pending stuff, if any
render(null, container);
document.body.removeChild(container);
container.innerHTML = '';
});

// https://jsfiddle.net/pnwLh7au/
it('Should have new state in render when changed state during setState + forceUpdate inside lifecycle methods and render only once', () => {
let updated = 0;

class Parent extends Component {
render() {
return (
<div>
<Child />
</div>
);
}
}

class Child extends Component {
state = {
foo: 'bar'
};

componentDidMount() {
this.setState({
foo: 'bar2'
});
this.forceUpdate();
}

componentDidUpdate() {
updated++;
}

render() {
return (
<div>
{this.state.foo}
</div>
);
}
}

render(<Parent />, container);
expect(container.firstChild.firstChild.innerHTML).toBe('bar');

rerender();

expect(container.firstChild.firstChild.innerHTML).toBe('bar2');
expect(updated).toBe(1);
});

it('Should ignore shouldComponentUpdate when forceUpdate called like React does', () => {
let updated = 0;

class Parent extends Component {
render() {
return (
<div>
<Child />
</div>
);
}
}

class Child extends Component {
state = {
foo: 'bar'
};

shouldComponentUpdate(prevProps, prevState) {
if (prevState.foo !== this.state.foo) {
return true;
}
return false;
}

componentDidMount() {
this.forceUpdate();
}

render() {
updated++;
return (
<div>
{this.state.foo}
</div>
);
}
}

render(<Parent />, container);
expect(container.firstChild.firstChild.innerHTML).toBe('bar');
expect(updated).toBe(1);

rerender();

expect(container.firstChild.firstChild.innerHTML).toBe('bar');
expect(updated).toBe(2);
});


// As per React https://jsfiddle.net/pnwLh7au/
// React has a different flow when setState is called outside lifecycle methods or event handlers (https://jsfiddle.net/egd1kuz6/),
// but inferno has another flow for setState and Inferno.
// Inferno collapses several `setState` even if they are called outside event listeners or lifecycle methods. So forceUpdate follows it
it('Should use the updated state when forceUpdate called like React does even if shouldComponentUpdate ignores it', () => {
let updated = 0;

class Parent extends Component {
render() {
return (
<div>
<Child />
</div>
);
}
}

class Child extends Component {
state = {
foo: 'bar'
};

shouldComponentUpdate() {
return false;
}

componentDidMount() {
this.setState({foo: 'bar2'})
this.forceUpdate();
}

render() {
updated++;
return (
<div>
{this.state.foo}
</div>
);
}
}

render(<Parent />, container);
expect(container.firstChild.firstChild.innerHTML).toBe('bar');
expect(updated).toBe(1);

rerender();

expect(container.firstChild.firstChild.innerHTML).toBe('bar2');
expect(updated).toBe(2);
});

// As per React https://jsfiddle.net/pnwLh7au/
it('Should use the updated state when forceUpdate called before setState like React does even if shouldComponentUpdate ignores it', () => {
let updated = 0;

class Parent extends Component {
render() {
return (
<div>
<Child />
</div>
);
}
}

class Child extends Component {
state = {
foo: 'bar'
};

shouldComponentUpdate() {
return false;
}

componentDidMount() {
this.forceUpdate();
this.setState({foo: 'bar2'})
}

render() {
updated++;
return (
<div>
{this.state.foo}
</div>
);
}
}

render(<Parent />, container);
expect(container.firstChild.firstChild.innerHTML).toBe('bar');
expect(updated).toBe(1);

rerender();

expect(container.firstChild.firstChild.innerHTML).toBe('bar2');
expect(updated).toBe(2);
});
});
8 changes: 7 additions & 1 deletion packages/inferno/src/core/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ function queueStateChanges<P, S>(component: Component<P, S>, newState: any, call
if (QUEUE.indexOf(component) === -1) {
QUEUE.push(component);
}
if (force) {
component.$F = true;
}
if (!microTaskPending) {
microTaskPending = true;
nextTick(rerender);
Expand Down Expand Up @@ -74,7 +77,9 @@ export function rerender() {

while ((component = QUEUE.shift())) {
if (!component.$UN) {
applyState(component, false);
const force = component.$F;
component.$F = false;
Copy link
Member

Choose a reason for hiding this comment

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

Why is this line needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As $F stores the information whether changes should be forced or not, we assume that they are not forced by default unless forceUpdate is called. The default value of $F is false.

If forceUpdate is called we switch $F to true, and wait before it will be time for our component to render. After the component updates, we want to return the default value for the component. So, it's why I'm using $F = false.

And finally, the component potentially could plan further changes via setState or even forceUpdate during its updating cycle. It means we want to set up all defaults before the component starts its updating cycle. So I cache value in const force.

applyState(component, force);

if (component.$QU) {
callSetStateCallbacks(component);
Expand Down Expand Up @@ -136,6 +141,7 @@ export class Component<P = {}, S = {}> implements IComponent<P, S> {
public $SSR?: boolean; // Server side rendering flag, true when rendering on server, non existent on client
public $L: Function[] | null = null; // Current lifecycle of this component
public $SVG: boolean = false; // Flag to keep track if component is inside SVG tree
public $F: boolean = false; // Force update flag

constructor(props?: P, context?: any) {
this.props = props || (EMPTY_OBJ as P);
Expand Down