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 support for getDerivedStateFromProps #1094

Merged

Conversation

andrewiggins
Copy link
Member

@andrewiggins andrewiggins commented May 8, 2018

Add support for the getDerivedStateFromProps lifecycle method.

Where componentWillMount or componentWillReceiveProps would've previously been called, Preact now invokes getDerivedStateFromProps on the component's constructor. The result of getDerivedStateFromProps is then immediately merged into the component's current state for use by the next lifecycle methods (primarily shouldComponentUpdate, render, and componentDidUpdate).

Sizes (bytes gzipped)
Before: 3443

After: 3477
Change: +34

commit 4150a25
After: 3480
Change: +37

Some differences from React

  1. React warns if getDerivedStateFromProps is defined on Functional Components (source).
  2. React warns if getDerivedStateFromProps is defined but state is not initialized (source). Preact always initializes state to an empty object (source) so this isn't necessary.
  3. React warns if getDerivedStateFromProps returns undefined (source). This PR allows undefined to be returned.

See comments for more discussion.

Updated sizes.

if (!component.base || mountAll) {
if (component.constructor.getDerivedStateFromProps) {
component.state = extend(component.state, component.constructor.getDerivedStateFromProps(props, component.state));
} else if (!component.base || mountAll) {
Copy link
Member Author

Choose a reason for hiding this comment

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

I suggest we make getDerivedStateFromProps mutually exclusive to componentWillMount and componentWillReceiveProps for two reasons:

  1. It saves 2 bytes! (okay not a real reason, but just thought I'd mention it :) ) No longer true after commit 4150a25
  2. React makes them mutually exclusive. And while Preact can differ from React, I think in this instance it might be better if follow their lead. Matching React's behavior prevents us from having to define, document, and maintain the interaction between getDerivedStateFromProps and the deprecated lifecycle methods.

Thoughts?

@@ -23,6 +23,206 @@ describe('Lifecycle methods', () => {
});


describe('static getDerivedStateFromProps', () => {
it('should set initial state with value returned from getDerivedStateFromProps', () => {
Copy link
Member Author

Choose a reason for hiding this comment

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

expect(element.className).to.be.equal('foo bar');
});

it('should update initial state with value returned from getDerivedStateFromProps', () => {
Copy link
Member Author

Choose a reason for hiding this comment

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

expect(element.className).to.equal('not-foo bar');
});

it('should update the instance\'s state with the value returned from getDerivedStateFromProps when props change', () => {
Copy link
Member Author

Choose a reason for hiding this comment

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

expect(Foo.prototype.componentDidUpdate).to.have.callCount(1); // verify update occurred
});

it('should NOT be invoked on state only updates', () => {
Copy link
Member Author

Choose a reason for hiding this comment

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

Choose a reason for hiding this comment

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

This might change in a minor/patch version. Related: facebook/react#12600 (comment).

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for the heads up! I'll take a look and see what we can do

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated to handle state-only updates

expect(Foo.getDerivedStateFromProps).to.have.been.calledOnce;
});

it('should NOT modify state if null is returned', () => {
Copy link
Member Author

Choose a reason for hiding this comment

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

});

// NOTE: Difference from React
// React v16.3.2 warns if undefined if returned from getDerivedStateFromProps
Copy link
Member Author

Choose a reason for hiding this comment

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


// TODO: Consider if componentWillUpdate should still be called
// Likely, componentWillUpdate should not be called only if getSnapshotBeforeUpdate is implemented
it('should NOT invoke deprecated lifecycles (cWM/cWRP) if new static gDSFP is present', () => {
Copy link
Member Author

Choose a reason for hiding this comment

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

@coveralls
Copy link

coveralls commented May 8, 2018

Coverage Status

Coverage remained the same at 100.0% when pulling 401261a on andrewiggins:getDerivedStateFromProps-2 into 4f57ba0 on developit:master.

@marvinhagemeister
Copy link
Member

@andrewiggins Thanks for making a PR for this 🎉 I have to read a bit more on the backstory of getDerivedStateFromProps to be able to review it. From a quick glance your implementation looks great! Love that you compared notes with the react tests and tried to make the changes as small as possible.

I'm currently lying in bed with an illness, but will review this once my health is back up again 🎉

@andrewiggins
Copy link
Member Author

@marvinhagemeister Hope you are feeling a little better! And no worries. I appreciate you circling back. I'm anticipating there will be more discussion and planning around what to do and how to deliver new lifecycle methods before we ship this.

Copy link
Member

@developit developit left a comment

Choose a reason for hiding this comment

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

Looks good. Let's see if we can nudge that size at all, otherwise I am happy with this. Thanks for your hard work!

}
else if (component.componentWillReceiveProps) {
component.componentWillReceiveProps(props, context);
if (typeof component.constructor.getDerivedStateFromProps === 'undefined') {
Copy link
Member

Choose a reason for hiding this comment

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

I'd be open to loosening this check if it can help offset the size.

Copy link
Member Author

Choose a reason for hiding this comment

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

By loosening do you mean making the check less specific (i.e if (!component.constructor.getDerivedStateFromProps))? I just tried that change in commit 3f51408 and it actually increased the size by 2 bytes.

Copy link
Member

Choose a reason for hiding this comment

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

@andrewiggins I think he meant removing the if-statement completely.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh, whoops. That makes sense. Yea, that would save about 4 bytes I believe.

@@ -73,6 +75,11 @@ export function renderComponent(component, opts, mountAll, isChild) {
skip = false,
rendered, inst, cbase;

let getDerivedStateFromProps = component.constructor.getDerivedStateFromProps;
Copy link
Member

Choose a reason for hiding this comment

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

It's usually smaller post-gzip to skip an intermediary variable like this one. Any chance that's the case here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Interesting, in this case it appears to not move the byte count at all. 3480 before and 3480 after the change (you can see it in commit b165370). So which do you prefer? Intermediary variable or not? Happy to update it to which ever.

Copy link
Member Author

Choose a reason for hiding this comment

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

Spoke too soon. When this change is combined with other changes, it could save an additional byte. I've updated the code to remove the intermediary variable.

@developit developit assigned developit and unassigned developit May 16, 2018
Copy link
Member

@marvinhagemeister marvinhagemeister left a comment

Choose a reason for hiding this comment

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

LGTM 🎉 @andrewiggins I'm really in awe of the thoroughness of this PR. You really went above all expectations by adding links to reacts unit tests and including them in our own suite.

Really solid and polished PR, thanks again for your contribution 🎉

}
else if (component.componentWillReceiveProps) {
component.componentWillReceiveProps(props, context);
if (typeof component.constructor.getDerivedStateFromProps === 'undefined') {
Copy link
Member

Choose a reason for hiding this comment

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

@andrewiggins I think he meant removing the if-statement completely.

@marvinhagemeister marvinhagemeister merged commit b4d7aee into preactjs:master May 19, 2018
@andrewiggins andrewiggins deleted the getDerivedStateFromProps-2 branch May 21, 2018 03:16
@andrewiggins
Copy link
Member Author

@marvinhagemeister Thanks for kind words 😄. I really appreciate the hard work @developit, you, and the other Preact contributors have put into building a high quality library in so few bytes. I want/hope my PRs meet that same bar.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants