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

State updated before call to shouldComponentUpdate in shallow render #1970

Closed
2 of 13 tasks
hisuwh opened this issue Jan 7, 2019 · 10 comments
Closed
2 of 13 tasks

State updated before call to shouldComponentUpdate in shallow render #1970

hisuwh opened this issue Jan 7, 2019 · 10 comments

Comments

@hisuwh
Copy link

hisuwh commented Jan 7, 2019

Current behavior

Given a simple component like this:

import * as React from "react";

interface Data {
    value: string;
}

export class SimpleComponent extends React.Component<Data, Data> {

    protected static getDerivedStateFromProps(props: Data, state: Data) {
        if (props.value === state.value) {
            return null;
        }

        console.log("updating state: ", props.value);
        return {
            value: props.value
        };
    }

    public state: Data = {
        value: this.props.value
    };

    public shouldComponentUpdate(nextProps: Data, nextState: Data) {
        console.log("state: ", this.state);
        console.log("nextState: ", nextState);
        console.log("props: ", this.props);
        console.log("nextProps: ", nextProps);
        const shouldUpdate = nextState.value !== this.state.value;
        console.log("shouldUpdate: ", shouldUpdate);
        return shouldUpdate;
    }

    public render() {
        console.log("render: ", this.state.value);
        return (
            <input value={this.props.value} />
        );
    }
}

I would expect the following test to pass:

import * as React from "react";
import { Expect, Test, TestFixture, FocusTest } from "alsatian";
import { shallow } from "enzyme";

import { SimpleComponent } from "./SimpleComponent";

@TestFixture("SimpleComponent")
export class SimpleComponentTests {

    @FocusTest
    @Test("should update value from props")
    public shouldUpdateValueFromProps() {

        const wrapper = shallow(<SimpleComponent value="initial" />);
        Expect(wrapper.find("input").prop("value")).toEqual("initial");
        wrapper.setProps({ value: "updated" });
        Expect(wrapper.find("input").prop("value")).toEqual("updated");
    }
}

However it fails as shouldComponentUpdate returns false. Looking at the logs we see the following:

# FIXTURE SimpleComponent
render:  initial
updating state:  updated
state:  { value: 'updated' }
nextState:  { value: 'updated' }
props:  { value: 'initial' }
nextProps:  { value: 'updated' }
shouldUpdate:  false
not ok 1 should update value from props
 ---
   message: "Expected \"initial\" to be equal to \"updated\"."
   severity: fail
   data:
     got: "initial"
     expect: "updated"
 ...

As you can see from the above this.state appears to have already been updated by the time shouldComponentUpdate gets called resulting in it returning false and the test failing as it never re-renders.

If I run the same component in the browser I get the following output:

render:  initial
updating state:  updated
state:  {value: "initial"}
nextState:  {value: "updated"}
props:  {value: "initial"}
nextProps:  {value: "updated"}
shouldUpdate:  true
render:  updated

Which is what I would expect.

Expected behavior

The state of the component should not be updated before calling shouldComponentUpdate.

Your environment

not sure whats needed here

API

  • shallow
  • mount
  • render

Version

library version
enzyme ^3.8.0
react ^16.3.0
react-dom ^16.5.0
react-test-renderer ~16.5.2
adapter (below) ^1.7.1

Adapter

  • enzyme-adapter-react-16
  • enzyme-adapter-react-16.3
  • enzyme-adapter-react-16.2
  • enzyme-adapter-react-16.1
  • enzyme-adapter-react-15
  • enzyme-adapter-react-15.4
  • enzyme-adapter-react-14
  • enzyme-adapter-react-13
  • enzyme-adapter-react-helper
  • others ( )
@ljharb
Copy link
Member

ljharb commented Jan 7, 2019

Try wrapper.update() after setProps?

@hisuwh
Copy link
Author

hisuwh commented Jan 8, 2019

@ljharb just tried it and it doesn't help

@chenesan
Copy link
Contributor

@hisuwh I see that shouldUpdate in the shouldComponentUpdate is computed by comparing props difference in the code snippet you provide , this should diff state right?

If so, I can reproduce that it works as expected behavior in browser(reproduce here). I also checked it will work in mount, and not work in shallow in enzyme test environment:

      class SimpleComponent extends React.Component {
        constructor(props) {
          super(props)
          this.state = {
            value: this.props.value
          }
        }
        static getDerivedStateFromProps(props, state) {
          if (props.value === state.value) {
            return null;
          }

          console.log("updating state: ", props.value);
          return {
            value: props.value
          };
        }

        shouldComponentUpdate(nextProps, nextState) {
          console.log("state: ", this.state);
          console.log("nextState: ", nextState);
          console.log("props: ", this.props);
          console.log("nextProps: ", nextProps);
          const shouldUpdate = nextState.value !== this.state.value;
          console.log("shouldUpdate: ", shouldUpdate);
          return shouldUpdate;
        }

        render() {
          console.log("render: ", this.state.value);
          return (
            <input value={this.props.value} />
          );
        }
      }
      // the test will pass with `mount`
      // won't pass with `shallow`
      const wrapper = mount(<SimpleComponent value="initial" />);
      expect(wrapper.find("input").prop("value")).to.equal("initial");
      wrapper.setProps({ value: "updated" });
      expect(wrapper.find("input").prop("value")).to.equal("updated");
    })

@hisuwh
Copy link
Author

hisuwh commented Jan 15, 2019

@chenesan yh it should have been state - I've corrected it.
I was testing to see if the same happened with props and didn't revert it before posting this sorry.

I have changed my test code to use mount as a workaround - though this is not ideal as I now need to handle more of the inner logic of the component.

Shallow is implementing most of these lifecycle methods correctly but not this one.

@chenesan
Copy link
Contributor

ok, I'll look into this.

@chenesan
Copy link
Contributor

After some investigation I found out that react-test-renderer/shallow doesn't handle set state in gDSFP properly. Have created an issue in facebook/react#14607 and waited for reply. If they regard this as a bug then I'll work on that.

@chenesan
Copy link
Contributor

@hisuwh the fix is merged into react and I think this issue should be fixed after react publishing new patch version.

@ljharb
Copy link
Member

ljharb commented Jan 18, 2019

@chenesan once the react-test-render change is released, would you open a PR with the relevant test cases, and a version bump? <3 please feel free to open the PR with tests ASAP :-D

@chenesan
Copy link
Contributor

I'll send PR after fix in react-test-renderer is released ;)

@hisuwh
Copy link
Author

hisuwh commented Mar 12, 2019

Thanks for your hard work @ljharb @chenesan

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

No branches or pull requests

3 participants