enzyme: componentDidUpdate is not called for shallow rendered component

npm package versions

enzyme: 3.2.0 enzyme-adapter-react-16: 1.1.1 react: 16.2.0 react-dom: 16.2.0

Problem

When changing the state inside a component, componentDidUpdate is not called.

Expected behaviour

When changing the state inside a component, componentDidUpdate should get called.

Code example

import { shallow } from 'enzyme';
import React from 'react';

class Dropdown extends React.Component {
    constructor(props) {
        super(props);

        console.log('constructor');

        this.state = {
            highlightedIndex: 0,
        };

        this.handleMouseOver = this.handleMouseOver.bind(this);
    }

    componentDidUpdate() {
        console.log('componentDidUpdate');
    }

    handleMouseOver() {
        console.log('handleMouseOver', 0);

        this.setState({
            highlightedIndex: 1,
        }, () => {
            console.log('handleMouseOver', 1);
        });

        console.log('handleMouseOver', 2);
    }

    render() {
        console.log('render');

        return <div onMouseOver={this.handleMouseOver}>Please hover me</div>;
    }
}

it('should work', () => {
    const wrapper = shallow(<Dropdown />);

    wrapper.simulate('mouseover');
    wrapper.update();
});

Console output

  console.log .../__tests__/Dropdown.js:8
    constructor

  console.log .../__tests__/Dropdown.js:34
    render

  console.log .../__tests__/Dropdown.js:22
    handleMouseOver 0

  console.log .../__tests__/Dropdown.js:34
    render

  console.log .../__tests__/Dropdown.js:27
    handleMouseOver 1

  console.log .../__tests__/Dropdown.js:30
    handleMouseOver 2

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 2
  • Comments: 28 (17 by maintainers)

Commits related to this issue

Most upvoted comments

@koba04, using the same example as above, the way that I implemented it is this:

MyComponent extends React.Component {
  componentDidUpdate(prevProps, prevState){
      // not called internally, but prevState and this.state will always match 
      // when called externally.
      console.log(prevState, this.state);
      if(prevState.thing !== this.state.thing){
        this.props.myCallback();
      }
  }

  updateThing(newThing){
    // doesn't call componentDidUpdate in enzyme 3.4.0
    this.setState({ thing: newThing });
    // ^^^ error caused by `this` context
  }
}

// enzyme test
spyOn(wrapper.instance(), 'setState').and.callFake(wrapper.setState); //mock the setState
wrapper.instance().updateThing('test');
// Error: ShallowWrapper::setState() can only be called on the root

Please note that your new code should make this problem irrelevant.

The fix that I used was to split the test:

spyOn(wrapper.instance, 'setState');
wrapper.instance().updateThing('test');
expect(wrapper.instance().setState).toHaveBeenCalledWith({thing: 'test'});

// test the componentDidUpdate separately
wrapper.setState({thing: 'test'});
expect(mockProps.myCallback).toHaveBeenCalled();

@ljharb Yes, that causes to rerender normally as ReactShallowRenderer, which means componentDidUpdate is never called because ReactShallowRenderer doesn’t support the lifecycle method. When setState is called from ReactComponent instance directly, it is processed in the React ShallowRenderer so enzyme can’t handle the update to call the lifecycle method.

Current lifecycle methods support is hooking update methods ShallowWrapper provides, which are shallowWrapper.setState or shallowWrapper.setProps or shallow() etc… So enzyme should be aware of that to do that.

@koba04 you are right, the issue I saw was inside of render() after a wrapper.update(). I will investigate the details and file a separate issue if necessary.