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

ShallowWrapper performs deep equality check in pureComponentShouldComponentUpdate #1915

Closed
2 of 13 tasks
rlee1121 opened this issue Nov 26, 2018 · 3 comments
Closed
2 of 13 tasks

Comments

@rlee1121
Copy link

rlee1121 commented Nov 26, 2018

Current behavior

ShallowWrapper uses lodash.isequal in the shouldComponentUpdate implementation of PureComponents. This is causing some minor discrepancy between React and Enzyme since _.isEqual performs a deep equality check whereas React, I believe, performs a shallow comparison on each member of the props/state objects. I'd imagine this has some performance implications as well.

I noticed this in a unit test when shallow rendering a PureComponent whose state uses ImmutableJS for some parts of it. In my unit test (and not in the actual web app), I was getting some unwanted warnings from ImmutableJS that were caused by the deep equality checks.

Test stack trace

console.warn node_modules/immutable/dist/immutable.js:4637
      iterable.length has been deprecated, use iterable.size or iterable.count(). This warning will become a silent error in a future version. Error
          at src_Map__Map.get (/home/travis/build/project/project/node_modules/immutable/dist/immutable.js:4632:21)
          at isArrayLike (/home/travis/build/project/project/node_modules/lodash.isequal/index.js:1588:42)
          at keys (/home/travis/build/project/project/node_modules/lodash.isequal/index.js:1806:10)
          at baseGetAllKeys (/home/travis/build/project/project/node_modules/lodash.isequal/index.js:883:16)
          at getAllKeys (/home/travis/build/project/project/node_modules/lodash.isequal/index.js:1286:10)
          at equalObjects (/home/travis/build/project/project/node_modules/lodash.isequal/index.js:1216:18)
          at baseIsEqualDeep (/home/travis/build/project/project/node_modules/lodash.isequal/index.js:994:10)
          at baseIsEqual (/home/travis/build/project/project/node_modules/lodash.isequal/index.js:935:10)
          at equalObjects (/home/travis/build/project/project/node_modules/lodash.isequal/index.js:1253:39)
          at baseIsEqualDeep (/home/travis/build/project/project/node_modules/lodash.isequal/index.js:994:10)
          at baseIsEqual (/home/travis/build/project/project/node_modules/lodash.isequal/index.js:935:10)
          at isEqual (/home/travis/build/project/project/node_modules/lodash.isequal/index.js:1639:10)
          at pureComponentShouldComponentUpdate (/home/travis/build/project/project/node_modules/enzyme/build/ShallowWrapper.js:169:82)
          at /home/travis/build/project/project/node_modules/enzyme/build/ShallowWrapper.js:599:30
          at withSetStateAllowed (/home/travis/build/project/project/node_modules/enzyme/build/Utils.js:326:3)
          at ShallowWrapper.<anonymous> (/home/travis/build/project/project/node_modules/enzyme/build/ShallowWrapper.js:576:42)
          at ShallowWrapper.single (/home/travis/build/project/project/node_modules/enzyme/build/ShallowWrapper.js:1830:25)
          at ShallowWrapper.setState (/home/travis/build/project/project/node_modules/enzyme/build/ShallowWrapper.js:575:14)
          at Component.ShallowWrapper.instance.setState (/home/travis/build/project/project/node_modules/enzyme/build/ShallowWrapper.js:217:35)

Also, I haven't really dived into the enzyme code prior to this, so apologies if I'm misunderstanding how ShallowWrapper works :)

Test case
In the tests at the bottom of the sample code, I'd have expected both test cases to pass (but only the second does). When I put this component into an existing app, I see the componentDidUpdate does get called when clicking on either button.

import React, { PureComponent } from 'react';

export default class Test extends PureComponent {
  state = {
    a: {
      b: {
        c: 1
      }
    }
  };

  componentDidUpdate(prevProps, prevState) {
    this.props.onUpdate();
  }

  // this should still trigger a re-render even though they have deep equality, but doesnt in enzyme
  handleClick = () => {
    this.setState({
      a: {
        b: {
          c: 1
        }
      }
    });
  };

  handleStateChangeClick = () => {
    this.setState({
      a: {
        b: {
          c: 2
        }
      }
    });
  };

  render() {
    return (
      <div>
        <button data={1} onClick={this.handleClick}>
          No State Change
        </button>
        <button data={2} onClick={this.handleStateChangeClick}>
          State Change
        </button>
      </div>
    );
  }
}

test('it re-renders when setState is called even if the object has deep equality to the existing state', () => {
  const updateSpy = jest.fn();
  const wrapper = shallow(<Test onUpdate={updateSpy} />);
  wrapper.find('button[data=1]').simulate('click');
  expect(updateSpy).toHaveBeenCalledTimes(1);
});

test('it re-renders when setState is called with an object that doesnt have deep equality', () => {
  const updateSpy = jest.fn();
  const wrapper = shallow(<Test onUpdate={updateSpy} />);
  wrapper.find('button[data=2]').simulate('click');
  expect(updateSpy).toHaveBeenCalledTimes(1);
});

Expected behavior

ShallowWrapper uses a shallow comparison as opposed to a deep comparison for its pureComponentShouldComponentUpdate check.

Your environment

API

  • shallow
  • mount
  • render

Version

library version
enzyme 3.7.0
react 16.6.0
react-dom 16.6.0
react-test-renderer 16.6.0
adapter (below)

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 Nov 26, 2018

Thanks for the report - could you provide a reproducible test case?

@rlee1121
Copy link
Author

@ljharb added a test case and also included the stack trace from my project's test output

@thomashuston
Copy link

For reference, here's React's shallowEqual helper used in shouldComponentUpdate of PureComponent:

function shallowEqual(objA: mixed, objB: mixed): boolean {
  if (is(objA, objB)) {
    return true;
  }


  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false;
  }


  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);


  if (keysA.length !== keysB.length) {
    return false;
  }


  // Test for A's keys different from B.
  for (let i = 0; i < keysA.length; i++) {
    if (
      !hasOwnProperty.call(objB, keysA[i]) ||
      !is(objA[keysA[i]], objB[keysA[i]])
    ) {
      return false;
    }
  }


  return true;
}

https://github.com/facebook/react/blob/144328fe81719e916b946e22660479e31561bb0b/packages/shared/shallowEqual.js#L36-L68

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